# Ripple Framework - AI/LLM Documentation ## Overview Ripple is a TypeScript UI framework that combines the best parts of React, Solid, and Svelte into one elegant package. Created by Dominic Gannaway ([@trueadm](https://github.com/trueadm)), Ripple is designed to be JS/TS-first with its own `.ripple` file extension that fully supports TypeScript. **Key Characteristics:** - **Performance**: Fine-grain rendering, with industry-leading performance, bundle-size and memory usage - **TypeScript-first**: Full TypeScript integration with type checking - **JSX-like syntax**: Familiar templating with Ripple-specific enhancements - **Reactive state**: Built-in reactivity with `track` and `@` reactive syntax - **Component-based**: Clean, reusable components with props and children ## Installation & Setup ```bash # Create new project from template npx degit trueadm/ripple/templates/basic my-app cd my-app npm i && npm run dev # Or install in existing project npm install ripple npm install --save-dev vite-plugin-ripple # For Vite integration ``` ## Core Syntax & Concepts ### Component Definition Components are defined using the `component` keyword (not functions that return JSX): ```ripple component Button(props: { text: string, onClick: () => void }) { } // Usage export component App() { } ``` Objects can also contain tracked values with `@` to access the reactive object property: ```ripple let counter = { current: track(0) }; counter.@current++; // Triggers reactivity ``` Tracked derived values are also `Tracked` objects, except you pass a function to `track` rather than a value: ```ripple let count = track(0); let double = track(() => @count * 2); let quadruple = track(() => @double * 2); console.log(@quadruple); ``` If you want to use a tracked value inside a reactive context, such as an effect but you don't want that value to be a tracked dependency, you can use `untrack`: ```ripple let count = track(0); let double = track(() => @count * 2); let quadruple = track(() => @double * 2); effect(() => { // This effect will never fire again, as we've untracked the only dependency it has console.log(untrack(() => @quadruple)); }) ``` > Note: you cannot create `Tracked` objects in module/global scope, they have to be created on access from an active component context. #### track with get / set The optional get and set parameters of the `track` function let you customize how a tracked value is read or written, similar to property accessors but expressed as pure functions. The get function receives the current stored value and its return value is exposed when the tracked value is accessed / unboxed with `@`. The set function should return the value that will actually be stored and receives two parameters: the first is the one being assigned and the second with the previous value. The get and set functions may be useful for tasks such as logging, validating, or transforming values before they are exposed or stored. ```ripple import { track } from 'ripple'; export component App() { let count = track(0, (current) => { console.log(current); return current; }, (next, prev) => { console.log(prev); if (typeof next === 'string') { next = Number(next); } return next; } ); } > Note: If no value is returned from either `get` or `set`, `undefined` is either exposed (for get) or stored (for set). Also, if only supplying the `set`, the `get` parameter must be set to `undefined`. #### trackSplit Function The `trackSplit` "splits" a plain object — such as component props — into specified tracked variables and an extra `rest` property containing the remaining unspecified object properties. ```ripple const [children, count, rest] = trackSplit(props, ['children', 'count']); ``` When working with component props, destructuring is often useful — both for direct use as variables and for collecting remaining properties into a `rest` object (which can be named arbitrarily). If destructuring happens in the component argument, e.g. `component Child({ children, value, ...rest })`, Ripple automatically links variable access to the original props — for example, `value` is compiled to `props.value`, preserving reactivity. However, destructuring inside the component body, e.g. `const { children, value, ...rest } = props`, for read-only reactive props, does not preserve reactivity (too complicated to implement due to many edge cases). To ensure destructured read-only reactive props remain reactive in this case, use the `trackSplit` function. > Note: boxed / wrapped Tracked objects are always reactive since they cross function boundaries by reference. Props that were not declared with `track()` are never reactive and always render the same value that was initially passed in. A full example utilizing various Ripple constructs demonstrates the `split` option usage: ```ripple import { track, trackSplit } from 'ripple'; import type { PropsWithChildren, Tracked } from 'ripple'; component Child(props: PropsWithChildren<{ count: Tracked, className: string }>) { // children, count are always reactive // but className is passed in as a read-only reactive value const [children, count, className, rest] = trackSplit(props, ['children', 'count', 'class']);
{`Count is: ${@count}`}
} export component App() { let count = track(0, (current) => { console.log('getter', current); return current; }, (next) => { console.log('setter', next); return next; } ); let className = track('shadow'); let name = track('Click Me'); function buttonRef(el) { console.log('ref called with', el); return () => { console.log('cleanup ref for', el); }; } { @name === 'Click Me' ? @name = 'Clicked' : @name = 'Click Me'; @className = ''}} {count} {ref buttonRef} >{@name}; } ``` With the regular destructuring, such as the one below, the `class` property would lose their reactivity: ```ripple // ❌ WRONG class / className reactivity would be lost let { children, count, class: className, ...rest } = props; ``` > Note: Make sure the resulting `rest`, if it's going to be spread onto a dom element, does not contain `Tracked` values. Otherwise, you'd be spreading not the actual values but the boxed ones, which are objects that will appear as `[object Object]` on the dom element. ### Transporting Reactivity **Critical Concept**: Ripple doesn't constrain reactivity to components only. `Tracked` objects can simply be passed by reference between boundaries to improve expressivity and co-location. #### Basic Transport Pattern ```ripple import { effect, track } from 'ripple'; function createDouble(count) { const double = track(() => @count * 2); effect(() => { console.log('Count:', @count) }); return double; } export component App() { let count = track(0); const double = createDouble(count);
{'Double: ' + @double}
} ``` #### Dynamic Component Transport Pattern Ripple has built-in support for dynamic components, a way to render different components based on reactive state. Instead of hardcoding which component to show, you can store a component in a `Tracked` via `track()`, and update it at runtime. When the tracked value changes, Ripple automatically unmounts the previous component and mounts the new one. Dynamic components are written with the `<@Component />` tag, where the @ both unwraps the tracked reference and tells the compiler that the component is dynamic. This makes it straightforward to pass components as props or swap them directly within a component, enabling flexible, state-driven UIs with minimal boilerplate. ```ripple export component App() { let swapMe = track(() => Child1); } component Child({ swapMe }: {swapMe: Tracked}) { <@swapMe /> } component Child1(props) {
{'I am child 1'}
} component Child2(props) {
{'I am child 2'}
} ``` **Transport Rules:** - Reactive state must be connected to a component - Cannot be global or created at module/global scope - Use arrays `[ trackedVar ]` or objects `{ trackedVar }` to transport reactivity - Functions can accept and return reactive state using these patterns - This enables composable reactive logic outside of component boundaries ### Control Flow #### If Statements ```ripple component Conditional({ isVisible }) {
if (isVisible) { {"Visible content"} } else { {"Hidden state"} }
} ``` #### For Loops ```ripple component List({ items }) {
    for (const item of items) {
  • {item.text}
  • }
} ``` #### For Loops with index ```ripple component ListView({ title, items }) {

{title}

    for (const item of items; index i) {
  • {item.text}{' at index '}{i}
  • }
} ``` #### Try-Catch (Error Boundaries) ```ripple component ErrorBoundary() {
try { } catch (e) {
{"Error: "}{e.message}
}
} ``` ### Children Components Use `children` prop for component composition: ```ripple import type { Component } from 'ripple'; component Card(props: { children: Component }) {
} // Usage

{"Card content here"}

``` ### Events #### Attribute Event Handling Events follow React-style naming (`onClick`, `onPointerMove`, etc.): ```ripple component EventExample() { let message = track("");
@message = e.target.value} />

{@message}

} ``` For capture phase events, add `Capture` suffix: - `onClickCapture` - `onPointerDownCapture` #### Direct Event Handling Use function `on` to attach events to window, document or any other element instead of addEventListener. This method guarantees the proper execution order with respect to attribute-based handlers such as `onClick`, and similarly optimized through event delegation for those events that support it. ```ripple import { effect, on } from 'ripple'; export component App() { effect(() => { // on component mount const removeListener = on(window, 'resize', () => { console.log('Window resized!'); }); // return the removeListener when the component unmounts return removeListener; }); } ``` ### Styling Components support scoped CSS with ` } ``` #### Dynamic Classes In Ripple, the `class` attribute can accept more than just a string — it also supports objects and arrays. Truthy values are included as class names, while falsy values are omitted. This behavior is powered by the `clsx` library. Examples: ```ripple let includeBaz = track(true);
// becomes: class="foo baz"
// becomes: class="foo bat" let count = track(3);
2}, @count > 3 && 'bat']}>
// becomes: class="foo bar" ``` #### Dynamic Inline Styles Sometimes you might need to dynamically set inline styles. For this, you can use the `style` attribute, passing either a string or an object to it: ```ripple let color = track('red');
const style = { @color, fontWeight: 'bold', 'background-color': 'gray', }; // using object spread
// using object directly
``` Both examples above will render the same inline styles, however, it's recommended to use the object notation as it's typically more performance optimized. > Note: When passing an object to the `style` attribute, you can use either camelCase or kebab-case for CSS property names. ### DOM References (Refs) Use `{ref fn}` syntax to capture DOM element references: ```ripple export component App() { let div = track(); const divRef = (node) => { @div = node; console.log("mounted", node); return () => { @div = undefined; console.log("unmounted", node); }; };
{"Hello world"}
} ``` Inline refs: ```ripple
console.log(node)}>{"Content"}
``` ## Built-in APIs ### Core Functions ```typescript import { mount, // Mount component to DOM track, // Create reactive state untrack, // Prevent reactivity tracking flushSync, // Synchronous state updates effect, // Side effects Context // Context API } from 'ripple'; ``` ### Mount API ```typescript mount(App, { props: { title: 'Hello world!' }, target: document.getElementById('root') }); ``` ### Effects ```ripple import { effect, track } from 'ripple'; export component App() { let count = track(0); effect(() => { console.log("Count changed:", @count); }); } ``` ### After Update tick() The `tick()` function returns a Promise that resolves after all pending reactive updates have been applied to the DOM. This is useful when you need to ensure that DOM changes are complete before executing subsequent code, similar to Vue's `nextTick()` or Svelte's `tick()`. ```ripple import { effect, track, tick } from 'ripple'; export component App() { let count = track(0); effect(() => { @count; if (@count === 0) { console.log('initial run, skipping'); return; } tick().then(() => { console.log('after the update'); }); }); } ``` ### Context Ripple has the concept of `context` where a value or reactive object can be shared through the component tree – like in other frameworks. This all happens from the `Context` class that is imported from `ripple`. Creating contexts may take place anywhere. Contexts can contain anything including tracked values or objects. However, context cannot be read via `get` or written to via `set` inside an event handler or at the module level as it must happen within the context of a component. A good strategy is to assign the contents of a context to a variable via the `.get()` method during the component initialization and use this variable for reading and writing. When Child components overwrite a context's value via `.set()`, this new value will only be seen by its descendants. Components higher up in the tree will continue to see the original value. Example with tracked / reactive contents: ```ripple import { track, Context } from "ripple" // create context with an empty object const context = new Context({}); const context2 = new Context(); export component App() { // get reference to the object const obj = context.get(); // set your reactive value obj.count = track(0); // create another tracked variable const count2 = track(0); // context2 now contains a trackrf variable context2.set(count2); // context's reactive property count gets updated
{'Context: '}{context.get().@count}
{'Context2: '}{@count2}
} ``` > Note: `@(context2.get())` usage with `@()` wrapping syntax will be enabled in the near future Passing data between components: ```ripple import { Context } from 'ripple'; const MyContext = new Context(null); component Child() { // Context is read in the Child component const value = MyContext.get(); // value is "Hello from context!" console.log(value); } component Parent() { const value = MyContext.get(); // Context is read in the Parent component, but hasn't yet // been set, so we fallback to the initial context value. // So the value is `null` console.log(value); // Context is set in the Parent component MyContext.set("Hello from context!"); } ``` ### Reactive Collections #### Simple Reactive Array Just like objects, you can use the `Tracked` objects in any standard JavaScript object, like arrays: ```ripple let first = track(0); let second = track(0); const arr = [first, second]; const total = track(() => arr.reduce((a, b) => a + @b, 0)); console.log(@total); ``` Like shown in the above example, you can compose normal arrays with reactivity and pass them through props or boundaries. However, if you need the entire array to be fully reactive, including when new elements get added, you should use the reactive array that Ripple provides. #### Fully Reactive Array `TrackedArray` class from Ripple extends the standard JS `Array` class, and supports all of its methods and properties. Import it from the `'ripple'` namespace or use the provided syntactic sugar for a quick creation via the bracketed notation. All elements existing or new of the `TrackedArray` are reactive and respond to the various array operations such as push, pop, shift, unshift, etc. Even if you reference a non-existent element, once it added, the original reference will react to the change. You do NOT need to use the unboxing `@` with the elements of the array. ```ripple import { TrackedArray } from 'ripple'; // using syntactic sugar `#` const arr = #[1, 2, 3]; // using the new constructor const arr = new TrackedArray(1, 2, 3); // using static from method const arr = TrackedArray.from([1, 2, 3]); // using static of method const arr = TrackedArray.of(1, 2, 3); ``` Usage Example: ```ripple export component App() { const items = new #[1, 2, 3];

{"Length: "}{items.length}

// Reactive length for (const item of items) {
{item}
}
} ``` #### Reactive Object `TrackedObject` class extends the standard JS `Object` class, and supports all of its methods and properties. Import it from the `'ripple'` namespace or use the provided syntactic sugar for a quick creation via the curly brace notation. `TrackedObject` fully supports shallow reactivity and any property on the root level is reactive. You can even reference non-existent properties and once added the original reference reacts to the change. You do NOT need to use the unboxing `@` with the properties of the `TrackedObject`. ```ripple import { TrackedObject } from 'ripple'; // using syntactic sugar `#` const arr = #{a: 1, b: 2, c: 3}; // using the new constructor const arr = new TrackedObject({a: 1, b: 2, c: 3}); ``` Usage Example: ```ripple export component App() { const obj = #{a: 0} obj.a = 0;
{'obj.a is: '}{obj.a}
{'obj.b is: '}{obj.b}
} ``` #### Reactive Set ```ripple import { TrackedSet } from 'ripple'; component SetExample() { const mySet = new TrackedSet([1, 2, 3]);

{"Size: "}{mySet.size}

// Reactive size

{"Has 2: "}{mySet.has(2)}

} ``` #### Reactive Map The `TrackedMap` extends the standard JS `Map` class, and supports all of its methods and properties. ```ripple import { TrackedMap, track } from 'ripple'; const map = new TrackedMap([[1,1], [2,2], [3,3], [4,4]]); ``` TrackedMap's reactive methods or properties can be used directly or assigned to reactive variables. ```ripple import { TrackedMap, track } from 'ripple'; export component App() { const map = new TrackedMap([[1,1], [2,2], [3,3], [4,4]]); // direct usage

{"Direct usage: map has an item with key 2: "}{map.has(2)}

// reactive assignment let has = track(() => map.has(2));

{"Assigned usage: map has an item with key 2: "}{@has}

} ``` #### Reactive Date The `TrackedDate` extends the standard JS `Date` class, and supports all of its methods and properties. ```ripple import { TrackedDate } from 'ripple'; const date = new TrackedDate(2026, 0, 1); // January 1, 2026 ``` TrackedDate's reactive methods or properties can be used directly or assigned to reactive variables. All getter methods (`getFullYear()`, `getMonth()`, `getDate()`, etc.) and formatting methods (`toISOString()`, `toDateString()`, etc.) are reactive and will update when the date is modified. ```ripple import { TrackedDate, track } from 'ripple'; export component App() { const date = new TrackedDate(2025, 0, 1, 12, 0, 0); // direct usage

{"Direct usage: Current year is "}{date.getFullYear()}

{"ISO String: "}{date.toISOString()}

// reactive assignment let year = track(() => date.getFullYear()); let month = track(() => date.getMonth());

{"Assigned usage: Year "}{@year}{", Month "}{@month}

} ``` ## Advanced Features ### Untracking Reactivity ```ripple import { untrack, track, effect } from 'ripple'; let count = track(0); let double = track(() => @count * 2); let quadruple = track(() => @double * 2); effect(() => { // This effect will never fire again, as we've untracked the only dependency it has console.log(untrack(() => @quadruple)); }) ``` ### Prop Shortcuts ```ripple // Object spread
{"Content"}
// Shorthand props (when variable name matches prop name)
{"Content"}
// Equivalent to:
{"Content"}
``` ### Raw HTML All text nodes are escaped by default in Ripple. To render trusted raw HTML strings, use the `{html}` directive. ```ripple export component App() { let source = `

My Blog Post

Hi! I like JS and Ripple.

`
{html source}
} ``` ## TypeScript Integration ### Component Types ```typescript import type { Component } from 'ripple'; interface Props { value: string; label: string; children?: Component; } component MyComponent(props: Props) { // Component implementation } ``` ### Context Types ```typescript type Theme = 'light' | 'dark'; const ThemeContext = new Context('light'); ``` ## File Structure ``` src/ App.ripple # Main app component components/ Button.ripple # Reusable components Card.ripple index.ts # Entry point with mount() ``` ## Development Tools ### VSCode Extension - **Name**: "Ripple for VS Code" - **ID**: `ripplejs.ripple-vscode-plugin` - **Features**: Syntax highlighting, diagnostics, TypeScript integration, IntelliSense ### Vite Plugin ```typescript // vite.config.js import { defineConfig } from 'vite'; import ripple from 'vite-plugin-ripple'; export default defineConfig({ plugins: [ripple()] }); ``` ### Prettier Plugin ```javascript // .prettierrc { "plugins": ["prettier-plugin-ripple"] } ``` ## Key Differences from Other Frameworks ### vs React - No JSX functions/returns - components use statement-based templates - Built-in reactivity with `track` and `@` syntax instead of useState/useEffect - Scoped CSS without CSS-in-JS libraries - No virtual DOM - fine-grained reactivity ### vs Svelte - TypeScript-first approach - JSX-like syntax instead of HTML templates - `.ripple` extension instead of `.svelte` - Similar reactivity concepts but different syntax ### vs Solid - Component definition with `component` keyword - Built-in collections (TrackedArray, TrackedSet) - Different templating approach within component bodies ## Best Practices 1. **Reactivity**: Use `track()` to create reactive variables and `@` to access them 2. **Strings**: Wrap string literals in `{"string"}` within templates 3. **Effects**: Use `effect()` for side effects, not direct reactive variable access 4. **Components**: Keep components focused and use TypeScript interfaces for props 5. **Styling**: Use scoped `