# Ripple - AI/LLM Documentation
## Overview
Ripple is a TypeScript-first UI framework and runtime for `.tsrx` files. TSRX is
the shared source language; Ripple is the target that provides fine-grained
reactivity, DOM rendering, server modules, hydration, context, portals, and
reactive collections.
When answering Ripple questions:
- Prefer `.tsrx` component examples.
- Return direct JSX for single-root components: `function Component(props) { return
; }`.
- Use JSX statement containers (`@{...}`) when TypeScript setup belongs next to the rendered output.
- Use JSX text for static text and `{expr}` for dynamic values.
- When a statement container mixes TypeScript setup with rendered output, put
setup first and finish it with one JSX element, JSX fragment, or JSX
control-flow expression. It cannot finish with a bare expression container,
and script statements cannot appear after the final output.
- Use `@if`, `@for`, `@switch`, and `@try` for rendered control flow. Their bodies
are implicit statement containers and must use `{...}` blocks.
- Keep runtime API guidance Ripple-specific, but keep syntax guidance aligned
with TSRX.
For target-neutral TSRX syntax, see https://tsrx.dev/llms.txt.
## Install
```bash
npm install ripple @ripple-ts/vite-plugin
```
```ts
// vite.config.ts
import { defineConfig } from 'vite';
import ripple from '@ripple-ts/vite-plugin';
export default defineConfig({
plugins: [ripple()],
});
```
Mount client apps with `mount()`.
```ts
import { mount } from 'ripple';
import { App } from './App.tsrx';
mount(App, {
target: document.getElementById('root'),
props: { title: 'Hello world' },
});
```
`mount()` renders the app under a default `try`/`pending`/`catch` boundary.
Pass `rootBoundary: { pending?, catch? }` to give it components, or
`rootBoundary: false` to render without one: `trackAsync()` must then sit inside
a user `@try` block, and errors that escape a `@try` propagate out of the flush.
Use `hydrate()` for server-rendered HTML that should become interactive.
```ts
import { hydrate } from 'ripple';
import { App } from './App.tsrx';
hydrate(App, {
target: document.getElementById('root'),
});
```
## Components
Components are TypeScript functions. Return a JSX element directly when there is
one root, use a fragment for real multiple-child output, and use a JSX statement
container (`@{...}`) when setup statements belong next to the UI.
```tsrx
export function Button({ text, onClick }: { text: string; onClick: () => void }) {
return {text} ;
}
export function App() {
return console.log('saved')} />;
}
```
When setup code and rendered output share the same scope, use `@{...}`. Setup
comes first and the statement container finishes with one output node: a JSX
element, JSX fragment, or JSX control-flow expression.
```tsrx
import { track } from 'ripple';
export function Counter() @{
let &[count] = track(0);
function increment() {
count++;
}
Count: {count}
}
```
If output needs multiple siblings, text, or expression containers after setup,
wrap that output in a fragment. Plain text between tags is JSX text, not
JavaScript.
```tsrx
export function LiteralText() {
return
x = 123
;
}
```
## Reactivity
Create reactive state with `track()` and lazy destructuring. Reading a lazy
binding subscribes to it; assigning to it updates the tracked value.
```tsrx
import { effect, track, type Tracked } from 'ripple';
export function Counter() @{
let &[count, countTracked] = track(0);
let &[double] = track(() => count * 2);
effect(() => {
console.log('count changed', count);
});
<>
Count: {count}
Double: {double}
count++}>Increment
>
}
function CounterValue({ count }: { count: Tracked }) {
return Shared value: {count.value}
;
}
```
You can also keep the tracked object and read or write `.value` directly.
```tsrx
import { track } from 'ripple';
export function Counter() @{
const count = track(0);
count.value++}>{count.value}
}
```
Use `untrack(fn)` when an effect or derived value needs to read something without
subscribing to it.
Use `snapshot(value)` to take a plain, detached shallow copy of a reactive array
or object. Values are read without subscribing, so it is safe to call inside an
effect or derived. The copy is a plain array or object (not a proxy); nested
values are shared by reference.
```tsrx
import { RippleObject, snapshot } from 'ripple';
export function Settings() @{
const settings = new RippleObject({ theme: 'dark', fontSize: 14 });
console.log(snapshot(settings))}>{'Log settings'}
}
```
## Reactive Collections
Use Ripple collection classes when collection operations should update rendered
output.
```tsrx
import { RippleArray, RippleMap, RippleObject, RippleSet } from 'ripple';
export function Inventory() @{
const products = new RippleArray(
{ id: 1, name: 'Jacket' },
{ id: 2, name: 'Boots' },
);
const prices = new RippleMap([[1, 120], [2, 95]]);
const selected = new RippleSet();
const totals = new RippleObject({ added: 0 });
<>
@for (const product of products; key product.id) {
{product.name}: ${prices.get(product.id)}
}
selected.add(1)}>Select jacket
Selected: {selected.size + totals.added}
>
}
```
Available collection APIs include `new RippleArray(...)`, `RippleArray.from(...)`,
`RippleArray.of(...)`, `new RippleObject(...)`, `new RippleMap(...)`, and
`new RippleSet(...)`.
## Template Control Flow
Rendered control flow uses directive expressions.
```tsrx
import { track } from 'ripple';
export function StatusBadge() @{
let &[status] = track<'idle' | 'loading' | 'done'>('idle');
<>
@switch (status) {
@case 'loading': {
Loading...
}
@case 'done': {
Done
}
@default: {
Idle
}
}
(status = 'done')}>Finish
>
}
```
Use `@for (... of ...)` for rendered lists. Filter the iterable before rendering
when some items should be skipped, and use `@empty { ... }` for the no-items
fallback. Direct `continue`, `break`, and `return` statements are not allowed in
`@for` template loop bodies, and are also invalid inside `@if` template branches.
`@switch` cases use `@case` and `@default` clauses with isolated `{...}` blocks.
They do not fall through, and do not use `break` or `return`.
```tsrx
export function UserList({ users }: { users: User[] }) @{
const visibleUsers = users.filter((user) => !user.hidden);
@for (const user of visibleUsers; index i; key user.id) {
{i + 1}. {user.name}
} @empty {
No users
}
}
```
Use ordinary TypeScript `return` for true component exits in setup code.
```tsrx
export function Profile({ user }: { user: User | null }) @{
if (!user) {
return null;
}
{user.name}
}
```
`@try` provides pending and error UI.
```tsrx
export function ProfileBoundary() @{
@try {
} @pending {
Loading...
} @catch (error, reset) {
Error: {error.message}
reset()}>Try again
}
}
```
## Events And Refs
Events use JSX-style handler props. DOM refs can be callback refs, tracked refs,
or local variables.
```tsrx
import { track } from 'ripple';
export function SearchBox() @{
let &[query] = track('');
let input: HTMLInputElement | undefined;
<>
Search
{
query = event.currentTarget.value;
}}
/>
input?.focus()}>Focus
>
}
```
## Dynamic Components And Elements
Use the dynamic tag syntax `<{expression}>` when the element tag or component
constructor is chosen at runtime. The expression can be a string tag name, a
component, or a tracked variable holding either. The closing tag repeats the
same expression: `{expression}>`. Ripple host elements use `class`, not
React's `className`.
```tsrx
import { track } from 'ripple';
type Tag = 'section' | 'article';
function Summary() {
return Summary
;
}
function Details() {
return Details ;
}
export function DynamicPanel() @{
let &[tag] = track('section' as Tag);
let &[Body] = track(() => Summary);
<>
<{tag} class="panel">
<{Body} />
{tag}>
{
tag = tag === 'section' ? 'article' : 'section';
Body = Body === Summary ? Details : Summary;
}}>
Swap
>
}
```
The tag expression must resolve to an element name: an identifier, member
access, static string, or a runtime expression composed of those. Calls,
spreads, string concatenation, and string interpolation are not valid tag
names. Do not use removed dynamic tag syntax such as `<@tag />`,
`<@Component />`, or the imported `Dynamic` component with an `is` prop. Use
`<{tag}>` instead.
## Styles
A `
>
}
```
- A block lives in the children list of an element or fragment, beside the
output it styles. A `@{ ... }` body or an `@if`/`@for`/`@switch`/`@try` branch
renders one output node and a block counts as one, so wrap the block and its
markup in a fragment: `<>
>`.
- Raw CSS in ``,
an ordinary element the compiler leaves alone.
- Blocks among the same children share one hash class and one stylesheet. A
nested element or fragment whose children hold a block is a nested scope with
its own hash; elements carry the hash of every scope around them, outer first.
Outer rules reach nested scopes; inner rules never reach out.
- A block inside an `@if`/`@for` branch styles only the elements that branch
renders, but its CSS always ships, because CSS is static.
- A selector that matches nothing the block can reach, including a selector
that only matches the containing element, is removed as unused and left as a
`/* (unused) ... */` comment.
- A scoped block accepts only the `ref` and `apply` attributes.
- Use `:global(...)` to reach outside the scope: `:global(.x)` is page-wide,
`.card :global(.x)` reaches only below your scoped `.card`, `:global(.dark)
.card` reacts to an ancestor's class, `.card:global(.is-open)` matches a class
another library toggles. `:global { ... }` does the same for every rule inside
it. `:global` may only start or end a selector. Prefer passing a class from a
theme to a child you own; keep `:global` for children you cannot change, with
a scoped selector in front:
| I want to ... | Use ... |
| ---------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| Style my own elements | A `;
// theme.$class -> 'tsrx-1a2b3c4d'
// theme.dark -> 'tsrx-1a2b3c4d dark'
export function Badge() {
return New ;
}
```
- A block that is exported, applied, or whose `$class` is read is a **theme**
and keeps every selector, element and descendant selectors included. A block
only read through its class keys keeps only its standalone class selectors.
- `$class` is reserved; a standalone `` applies the
theme and declares the scope's own block in one tag.
```tsrx
import { theme } from './theme.tsrx';
export function Panel() @{
<>
Purple
Black: the local rule beats the theme's green
>
}
```
- `apply={[a, b]}` applies several themes; `const accent = `)
.replace('', body);
```
### Streaming SSR
With `stream` set, `render` flushes a shell immediately: all synchronous
content, each suspended `@try` boundary showing its `@pending` fallback, and
all CSS registered so far. As each boundary's async work settles, its HTML
streams as a self-contained chunk — carrying its own CSS, serialized
`trackAsync` results, and `` content — and an inline runtime swaps it
into place. Chunks arrive out of order, parents always before children.
Catch-only boundaries stream an empty slot that later resolves to the body or
the server-rendered `@catch` HTML. `hydrate()` handles both arrival orders:
content swapped before hydration hydrates normally, and later chunks activate
their boundary in place without re-rendering.
```ts
import { render, createStream } from 'ripple/server';
const { stream, sink } = createStream();
render(App, {
stream: sink,
rootBoundary: { pending: Loading },
streamTemplate: {
before: '',
between: '',
after: '
',
},
});
return new Response(stream, { headers: { 'Content-Type': 'text/html; charset=utf-8' } });
```
Apps built on `@ripple-ts/vite-plugin` enable streaming for render routes in
`ripple.config.ts` instead of calling `render` directly:
```ts
export default {
ssr: {
streaming: true,
},
};
```
`index.html` must keep the `` and `` markers;
the plugin falls back to buffered SSR with a warning when they are missing.
## Raw HTML
Use `innerHTML={trustedHtml}` for trusted markup. Do not pass untrusted strings to
raw HTML props.
```tsrx
export function Article({ markup }: { markup: string }) {
return ;
}
```
## Do And Do Not
Do:
- Use `.tsrx` for component files.
- Return single-root components directly in examples.
- Use `@{...}` when setup appears next to rendered output, and finish the
statement container with one JSX element, JSX fragment, or JSX control-flow expression.
- Use JSX text for literal children.
- Use `@for` for rendered lists and `@if` for conditional rendering.
- Use Ripple collections for reactive collection state.
- Put a `