Json-render svelte
Svelte 5 renderer for json-render that turns JSON specs into Svelte component trees. Use when working with @json-render/svelte, building Svelte UIs from JSON, creating component catalogs, or rendering AI-generated specs.
install
source · Clone the upstream repo
git clone https://github.com/vercel-labs/json-render
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/vercel-labs/json-render "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/svelte" ~/.claude/skills/vercel-labs-json-render-svelte && rm -rf "$T"
manifest:
skills/svelte/SKILL.mdsource content
@json-render/svelte
Svelte 5 renderer that converts json-render specs into Svelte component trees.
Quick Start
<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
import { 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>:
interface 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 }
<!-- 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>
<!-- 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
import { 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:
{ "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:
- truthy check{ "$state": "/path" }
- equality check{ "$state": "/path", "eq": value }
- falsy check{ "$state": "/path", "not": true }
- AND conditions{ "$and": [cond1, cond2] }
- OR conditions{ "$or": [cond1, cond2] }
Providers (via JsonUIProvider)
JsonUIProvider composes all contexts. Individual contexts:
| Context | Purpose |
|---|---|
| Share state across components (JSON Pointer paths) |
| Handle actions dispatched via the event system |
| Enable conditional rendering based on state |
| Form field validation |
Event System
Components use
emit to fire named events. The element's on field maps events to action bindings:
<!-- 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>
{ "type": "Button", "props": { "label": "Submit" }, "on": { "press": { "action": "submit" } } }
Built-in Actions
The
setState action is handled automatically and updates the state model:
{ "action": "setState", "actionParams": { "statePath": "/activeTab", "value": "home" } }
Other built-in actions:
pushState, removeState, push, pop.
Dynamic Props and Two-Way Binding
Expression forms resolved before your component receives props:
- read from state{"$state": "/state/key"}
- read + write-back to state{"$bindState": "/form/email"}
- read + write-back for repeat items{"$bindItem": "field"}
- conditional value{"$cond": <condition>, "$then": <value>, "$else": <value>}
For writable bindings inside components, use
getBoundProp:
<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:
- returnsgetStateValue(path)
(read/write){ current }
- returnsgetBoundProp(() => value, () => bindingPath)
(read/write when bound){ current }
- returnsisVisible(condition)
(boolean){ current }
- returnsgetAction(name)
(registered handler){ current }
Advanced context access:
getStateContext()getActionContext()getVisibilityContext()getValidationContext()getOptionalValidationContext()getFieldValidation(ctx, path, config?)
Streaming UI
Use
createUIStream for spec streaming:
<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:
const chat = createChatUI({ api: "/api/chat-ui" }); await chat.send("Build a settings panel");