@json-render/svelte
Svelte 5 renderer that converts json-render specs into Svelte component trees.
Quick Start
svelte<script lang="ts"> import { Renderer, JsonUIProvider } from "@json-render/svelte"; import type { Spec } from "@json-render/svelte"; import Card from "./components/Card.svelte"; import Button from "./components/Button.svelte"; interface Props { spec: Spec | null; } let { spec }: Props = $props(); const registry = { Card, Button }; </script> <JsonUIProvider> <Renderer {spec} {registry} /> </JsonUIProvider>
Creating a Catalog
typescriptimport { defineCatalog } from "@json-render/core"; import { schema } from "@json-render/svelte"; import { z } from "zod"; export const catalog = defineCatalog(schema, { components: { Button: { props: z.object({ label: z.string(), variant: z.enum(["primary", "secondary"]).nullable(), }), description: "Clickable button", }, Card: { props: z.object({ title: z.string() }), description: "Card container with title", }, }, });
Defining Components
Components should accept BaseComponentProps<TProps>:
typescriptinterface BaseComponentProps<TProps> { props: TProps; // Resolved props for this component children?: Snippet; // Child elements (use {@render children()}) emit: (event: string) => void; // Fire a named event bindings?: Record<string, string>; // Map of prop names to state paths (for $bindState) loading?: boolean; // True while spec is streaming }
svelte<!-- Button.svelte --> <script lang="ts"> import type { BaseComponentProps } from "@json-render/svelte"; interface Props extends BaseComponentProps<{ label: string; variant?: string }> {} let { props, emit }: Props = $props(); </script> <button class={props.variant} onclick={() => emit("press")}> {props.label} </button>
svelte<!-- Card.svelte --> <script lang="ts"> import type { Snippet } from "svelte"; import type { BaseComponentProps } from "@json-render/svelte"; interface Props extends BaseComponentProps<{ title: string }> { children?: Snippet; } let { props, children }: Props = $props(); </script> <div class="card"> <h2>{props.title}</h2> {#if children} {@render children()} {/if} </div>
Creating a Registry
typescriptimport { defineRegistry } from "@json-render/svelte"; import { catalog } from "./catalog"; import Card from "./components/Card.svelte"; import Button from "./components/Button.svelte"; const { registry, handlers, executeAction } = defineRegistry(catalog, { components: { Card, Button, }, actions: { submit: async (params, setState, state) => { // handle action }, }, });
Spec Structure (Element Tree)
The Svelte schema uses the element tree format:
json{ "root": "card1", "elements": { "card1": { "type": "Card", "props": { "title": "Hello" }, "children": ["btn1"] }, "btn1": { "type": "Button", "props": { "label": "Click me" } } } }
Visibility Conditions
Use visible on elements to show/hide based on state:
{ "$state": "/path" }- truthy check{ "$state": "/path", "eq": value }- equality check{ "$state": "/path", "not": true }- falsy check{ "$and": [cond1, cond2] }- AND conditions{ "$or": [cond1, cond2] }- OR conditions
Providers (via JsonUIProvider)
JsonUIProvider composes all contexts. Individual contexts:
| Context | Purpose |
|---|---|
StateContext | Share state across components (JSON Pointer paths) |
ActionContext | Handle actions dispatched via the event system |
VisibilityContext | Enable conditional rendering based on state |
ValidationContext | Form field validation |
Event System
Components use emit to fire named events. The element's on field maps events to action bindings:
svelte<!-- Button.svelte --> <script lang="ts"> import type { BaseComponentProps } from "@json-render/svelte"; interface Props extends BaseComponentProps<{ label: string }> {} let { props, emit }: Props = $props(); </script> <button onclick={() => emit("press")}>{props.label}</button>
json{ "type": "Button", "props": { "label": "Submit" }, "on": { "press": { "action": "submit" } } }
Built-in Actions
The setState action is handled automatically and updates the state model:
json{ "action": "setState", "actionParams": { "statePath": "/activeTab", "value": "home" } }
Other built-in actions: pushState, removeState, push, pop.
Dynamic Props and Two-Way Binding
Nested lists can set repeat.statePath to { "$item": "field" } inside an enclosing repeat.
Expression forms resolved before your component receives props:
{"$state": "/state/key"}- read from state{"$bindState": "/form/email"}- read + write-back to state{"$bindItem": "field"}- read + write-back for repeat items{"$cond": <condition>, "$then": <value>, "$else": <value>}- conditional value
For writable bindings inside components, use getBoundProp:
svelte<script lang="ts"> import { getBoundProp } from "@json-render/svelte"; import type { BaseComponentProps } from "@json-render/svelte"; interface Props extends BaseComponentProps<{ value?: string }> {} let { props, bindings }: Props = $props(); let value = getBoundProp<string>( () => props.value, () => bindings?.value, ); </script> <input bind:value={value.current} />
Context Helpers
Preferred helpers:
getStateValue(path)- returns{ current }(read/write)getBoundProp(() => value, () => bindingPath)- returns{ current }(read/write when bound)isVisible(condition)- returns{ current }(boolean)getAction(name)- returns{ current }(registered handler)
Advanced context access:
getStateContext()getActionContext()getVisibilityContext()getValidationContext()getOptionalValidationContext()getFieldValidation(ctx, path, config?)
Streaming UI
Use createUIStream for spec streaming:
svelte<script lang="ts"> import { createUIStream, Renderer } from "@json-render/svelte"; const stream = createUIStream({ api: "/api/generate-ui", onComplete: (spec) => console.log("Done", spec), }); async function generate() { await stream.send("Create a login form"); } </script> <button onclick={generate} disabled={stream.isStreaming}> {stream.isStreaming ? "Generating..." : "Generate UI"} </button> {#if stream.spec} <Renderer spec={stream.spec} {registry} loading={stream.isStreaming} /> {/if}
Use createChatUI for chat + UI responses:
typescriptconst chat = createChatUI({ api: "/api/chat-ui" }); await chat.send("Build a settings panel");

