A Svelte 5 runtime that turns a JSON spec into a live, interactive UI.
Ripple is the rendering layer for AI-generated interfaces. An LLM (or any other source) emits a small JSON document describing what the UI should look like; Ripple mounts it as a real Svelte component tree with state, two-way bindings, expression evaluation, and event handling. The model writes structure; Ripple handles reactivity.
You don't author Svelte components per screen. You hand Ripple a spec and it builds the UI.
{
"version": "1.0",
"state": { "name": "" },
"ui": {
"type": "flex",
"props": { "direction": "column", "gap": "12px" },
"children": [
{ "type": "input", "bind": "name", "props": { "label": "Your name" } },
{ "type": "text", "props": { "text": "Hello, {state.name}!" } }
]
}
}That spec is a fully working two-way-bound form. No glue code.
Three things, in order of how often you'll touch them:
-
Renders 150+ typed widgets from JSON. Every node in the tree (
{ "type": "...", "props": {...}, "children": [...] }) maps to a Svelte component βflex,card,kanban,chart,comparison-layout,wizard-layout,pricing-table, all of them. The full catalog with prop schemas lives indist/manifest.json. -
Wires state and interactivity declaratively.
stateis a top-level object on the spec. Inputs use"bind": "<state-path>"for two-way binding. Buttons fireon_clickaction chains (set,push,remove,toggle,validate,branch,confirm,api,navigate,toast,emit). Expressions inside any string β{state.count + 1},{state.x > 0 ? 'yes' : 'no'},{item.price}β are resolved at render time against state and loop context. -
Hands back events. Anything the host needs to handle (HTTP calls, navigation, toasts, custom signals) bubbles out via a single
onEventcallback on the<Ripple>component. The spec doesn't reach into your app; it asks the host to do something.
The pipeline in five steps:
spec (JSON)
β
βΌ
ββββββββββββββββ UISpec β UniversalSpec wrapper
β normalizer β (or pass-through if already universal)
ββββββββ¬ββββββββ
βΌ
ββββββββββββββββ Svelte 5 $state proxy with dot-notation
β StateManager β `get("user.profile.name") / set("count", 7)`
ββββββββ¬ββββββββ
βΌ
ββββββββββββββββ Resolves `{state.x + 1}`, `{item.field}`,
β Expression β ternaries, comparisons, method calls.
β Resolver β Tracks reads inside derived blocks for
ββββββββ¬ββββββββ automatic reactivity.
βΌ
ββββββββββββββββ Walks the tree, picks the Svelte component
β NodeRenderer β from the registry per `type`, evaluates
ββββββββ¬ββββββββ `show`/`bind`/`on_*`, renders children.
βΌ
ββββββββββββββββ User clicks β `on_click` chain runs through
β Event β the dispatcher: state mutations stay local,
β Dispatcher β side-effecting actions (api/toast/navigate)
ββββββββββββββββ fire `onEvent` on the host.
Each piece is independent and Svelte 5 native:
- Normalizer (
src/lib/core/normalizer.ts) accepts either spec format and returns aUniversalSpec. - StateManager (
src/lib/core/state-manager.svelte.ts) wraps a Svelte 5$stateproxy. Path-based reads/writes auto-create intermediate objects. Subscribe viastateManager.subscribe(callback). - ExpressionResolver (
src/lib/core/expression-resolver.ts) parses{...}strings β paths, arithmetic, comparisons, ternaries, optional chaining, whitelisted methods (.includes,.length,.toFixed, etc.). Detects UINode-shaped subtrees and leaves them raw so they can resolve later in their own loop context. - EventDispatcher (
src/lib/core/event-dispatcher.ts) executes action chains. State actions (set/push/remove/toggle) write through StateManager; control actions (branch/validate/confirm) gate further actions; effect actions (api/navigate/toast/emit/pin/unpin) emitRippleEvents the host can intercept. - NodeRenderer (
src/lib/components/NodeRenderer.svelte) is the recursive walker. It evaluatesshowconditions, resolves props, builds loop contexts foreach, managesbindtwo-way wiring, and renders children β including named slots (header,footer,sidebar,topbar,actions). - Widget registry (
src/lib/widgets/index.ts) mapstypestrings to Svelte components. Aliases are flat entries (e.g.dialog β Modal,comparison-cards β ComparisonLayout). Register your own at runtime withregisterWidget(type, component).
Reactivity is end-to-end: every state mutation flows through the proxy, Svelte recomputes any derived that read it, and only the affected widget re-renders. There is no virtual DOM diff phase you have to think about β it's the same system Svelte 5 uses for hand-written components.
Three reasons that compound:
- LLMs produce structured data better than they produce code. A model emits a spec like the one above with high reliability; the same model writing equivalent Svelte code is several times more error-prone.
- Specs are inspectable, diffable, persistable. You can save a UI to a database, send it over the wire, replay it from a log, run a regression test against it. JSX-as-rendered output gives you none of that.
- The contract is small. Every widget's prop schema lives in
dist/manifest.json(150 widgets, ~270 KB). A model can fetch it once and have a complete catalog, including runnable examples. New widgets ship by adding to the registry β no SDK update on the agent side.
- JSON-driven rendering β Define UIs as data, not code
- Two spec formats β Low-level UISpec (v1.0) for full control, or high-level UniversalSpec (v2.0) for intent-based UIs
- 150+ built-in widgets across nine categories β layout, display, input, data, overlay, control, composite, research, and enterprise verticals
- Reactive expressions β
{state.user.name}syntax with comparisons, ternary, and logical operators - State management β Svelte 5 rune-based with dot-notation path access
- Event system β Declarative handlers for state updates, API calls, navigation, toasts, and custom events
- Intent system β Auto-layout engine, pattern detection (quiz, charts, results), and multi-step flow chaining
- LLM-ready manifest β
dist/manifest.jsonships with every release, declaring every widget's prop schema and a runnable example. Agents can fetch it at runtime to learn the API - Extensible β Register custom widgets at runtime
- Theming β shadcn-svelte tokens with full color/radius/mode overrides
bun add @ripple-ui/svelteRequires Svelte 5 (^5.0.0).
<script lang="ts">
import { Ripple } from '@ripple-ui/svelte';
const spec = {
version: '1.0',
state: { count: 0 },
ui: {
type: 'flex',
props: { direction: 'column', gap: 4 },
children: [
{ type: 'heading', props: { text: 'Counter', level: 2 } },
{ type: 'text', props: { text: 'Count: {state.count}' } },
{
type: 'button',
props: { label: '+1' },
on_click: { action: 'set', target: 'count', value: '{state.count}' }
}
]
}
};
</script>
<Ripple {spec} />Explicit widget tree with props, events, and control flow:
{
"version": "1.0",
"state": { "query": "" },
"ui": {
"type": "flex",
"props": { "direction": "column", "gap": 3 },
"children": [
{ "type": "input", "props": { "placeholder": "Search..." }, "bind": "{state.query}" },
{
"type": "if",
"condition": "{state.query != ''}",
"children": [{ "type": "text", "props": { "text": "Searching: {state.query}" } }]
}
]
}
}Declare what the UI should do, and Ripple picks the layout:
{
"version": "2.0",
"intent": "browse",
"title": "Products",
"data": {
"items": [
{ "id": "1", "name": "Widget", "image": "/img/widget.jpg", "price": "$9.99" }
]
},
"fields": { "title": "name", "image": "image", "subtitle": "price" },
"selection": "single"
}| Category | Examples | Count |
|---|---|---|
| Layout | flex, grid, card, tabs, accordion, split, master-detail, app-shell, sidebar, page-header, hero, section, breadcrumb, dashboard |
21 |
| Display | text, heading, image, badge, metric, stat, progress, progress-ring, avatar, markdown, code-block, kbd, chip, quote, definition-list, comparison-table, pros-cons, steps, link-preview, qr, diff |
33 |
| Input | button, input, textarea, select, combobox, multi-select, checkbox, switch, radio-group, slider, rating, date-picker, time-picker, number-input, segmented, color-picker, file-upload, form, filter-bar, search, location-picker |
24 |
| Data | table, data-grid, chart, kanban, gantt, calendar, timeline, tree, tree-table, virtual-list, sparkline, gauge, funnel, heatmap, sankey, treemap, map |
18 |
| Overlay | alert, callout, tooltip, popover, dropdown-menu, toast, command-palette, context-menu, notification-center, error-state, coachmark |
12 |
| Composite layouts | comparison-layout, entity-detail, form-layout, wizard-layout, checklist-layout, report-layout, invoice-layout, order-status, plus dashboard variants (exec/ops/analytics/pipeline/project), terminal, workflow, c4 |
16 |
| Research | source-card, citation, sources-bar, discover-card, follow-up, kv-table, news-card, ticker, company-header, callout, analyst-bar, range-bar |
13 |
| Vertical | pricing-table, settings-list, comment-thread, audit-log, api-key, people-picker, permission-matrix, org-chart, invoice-lines, bulk-action-bar, saved-views |
11 |
| Control | if, each |
2 |
The complete prop schema and a runnable example for every widget live in dist/manifest.json (regenerated by bun run build:manifest). LLM agents fetch this manifest to learn the API; treat it as the source of truth.
Reactive bindings using {expression} syntax:
{state.user.name} β State path
{item.price} β Loop variable
{state.count > 0} β Comparison
{state.active ? 'On' : 'Off'} β Ternary
{state.a && state.b} β Logical AND
{!state.loading} β Negation
Declarative handlers with 8 action types:
{
"on_click": [
{ "action": "set", "target": "loading", "value": true },
{ "action": "api", "url": "/api/save", "method": "POST" },
{ "action": "toast", "message": "Saved!", "variant": "success" }
]
}| Action | Behavior |
|---|---|
set |
Update state |
open |
Set state to true (dialog shorthand) |
api |
HTTP request (emitted to parent) |
navigate |
URL navigation (emitted to parent) |
toast |
Show notification (emitted to parent) |
emit |
Custom event (emitted to parent) |
pin / unpin |
Sidebar persistence (emitted to parent) |
import { registerWidget } from '@ripple-ui/svelte';
import MyWidget from './MyWidget.svelte';
registerWidget('my-widget', MyWidget);bun install # Install dependencies
bun run dev # Dev server with playground (also serves /manifest.json)
bun run build # Build library + manifest to dist/
bun run build:manifest # Regenerate dist/manifest.json and static/manifest.json
bun run check # Type-check
bun run test # Run testsThe dev server serves the widget manifest at http://localhost:5174/manifest.json. Point your LLM tooling at this URL to develop against live changes.
Full documentation in docs/:
- Getting Started
- Architecture
- UISpec Reference
- UniversalSpec Reference
- Widgets
- Expressions
- State Management
- Event Handling
- Intent System
- Theming
- Custom Widgets
- API Reference
MIT