Skip to main content
OrchestKit v7.85.0 โ€” 107 skills, 37 agents, 188 hooks ยท Claude Code 2.1.132+
OrchestKit
Skills

Json Render Catalog

json-render component catalog patterns for AI-safe generative UI. Define Zod-typed catalogs that constrain what AI can generate, use @json-render/shadcn for 36 pre-built components, optimize specs with YAML mode, and apply the three edit modes (patch/merge/diff) for progressive updates. Use when building AI-generated UIs, defining component catalogs, or integrating json-render into React/Vue/Svelte/React Native/Ink/Next.js projects.

Reference medium

Auto-activated โ€” this skill loads automatically when Claude detects matching context.

json-render Component Catalogs

json-render (Vercel Labs, 12.9K stars, Apache-2.0) is a framework for AI-safe generative UI. AI generates flat-tree JSON (or YAML) specs constrained to a developer-defined catalog โ€” the catalog is the contract between your design system and AI output. If a component or prop is not in the catalog, AI cannot generate it.

Storybook โ†’ catalog import (#1529, 2026-04)

When the project ships a Storybook setup, import the catalog from Storybook stories instead of hand-writing one. The bundled importer at scripts/storybook-to-catalog.mjs reads a @storybook/addon-mcp list-all-documentation manifest and emits a Zod-typed catalog.ts plus a components.tsx registry.

node "${CLAUDE_SKILL_DIR}/scripts/storybook-to-catalog.mjs" storybook-manifest.json \
  --out src/genui/catalog.ts \
  --components src/genui/components.tsx \
  --project-root .

Storybook becomes the single source of truth โ€” adding a story automatically expands the AI-allowed surface; removing one shrinks it. AI safety is enforced at import: callbacks, raw object props, and z.any() are dropped. Full mapping: references/storybook-import.md. Companion fixture for testing: references/storybook-fixture.json.

New in 2026-04 (json-render 0.14 โ†’ 0.18)

  • Devtools ecosystem (0.18) โ€” five new packages: @json-render/devtools core + framework adapters for React, Vue, Svelte, Solid. Inspector panel has six tabs (Spec, State, Actions, Stream, Catalog, Pick) with DOM element picking that maps back to spec keys. Tree-shakes to null in production. Companion Next.js demo app shipped with AI-chat + catalog integration. Action observer infrastructure exposed for adapters to mirror events into the panel.
  • Zod 4 fix (0.18) โ€” formatZodType now correctly handles z.record(), z.default(), and z.literal() (previously produced empty/wrong prompt output).
  • Three edit modes (0.14) โ€” patch (RFC 6902), merge (RFC 7396), diff (unified) for progressive AI refinements. buildEditUserPrompt() + diffToPatches() + deepMergeSpec() in @json-render/core.
  • @json-render/yaml (0.14) โ€” official YAML wire format + streaming parser; buildUserPrompt(\{ format: 'yaml' \}).
  • @json-render/ink (0.15) โ€” render catalogs to terminal UIs (Ink-based, 20+ components) using the same spec.
  • @json-render/next (0.16) โ€” generate full Next.js apps (routes, layouts, SSR, metadata) from a single spec.
  • @json-render/shadcn-svelte (0.16) โ€” 36-component Svelte 5 + Tailwind mirror of the React shadcn catalog.
  • shadcn catalog at 36 components (was documented as 29 โ€” the count was wrong even at 0.13). Use @json-render/shadcn as-is or mergeCatalogs() with your custom types.
  • @json-render/react-three-fiber now ships 20 components including GaussianSplat (0.17).
  • @json-render/mcp โ€” upgrade plain MCP tool JSON into interactive iframes inside Claude/Cursor/ChatGPT conversations. See the ork:mcp-visual-output skill.
  • MCP multi-surface: same spec renders to React, PDF (@json-render/react-pdf), email (@json-render/react-email), terminal (Ink), Next.js apps, and Remotion videos.

Quick Reference

CategoryRulesImpactWhen to Use
Catalog Definition1HIGHDefining component catalogs with Zod
Prop Constraints1HIGHConstraining AI-generated props for safety
shadcn Catalog1MEDIUMUsing pre-built shadcn components
Token Optimization1MEDIUMReducing token usage with YAML mode
Actions & State1MEDIUMAdding interactivity to specs

Total: 5 rules across 5 categories

How json-render Works

  1. Developer defines a catalog โ€” Zod-typed component definitions with constrained props
  2. AI generates a spec โ€” flat-tree JSON/YAML referencing only catalog components
  3. Runtime renders the spec โ€” <Render> component validates and renders each element

The catalog is the safety boundary. AI can only reference types that exist in the catalog, and props are validated against Zod schemas at runtime. This prevents hallucinated components and invalid props from reaching the UI.

Quick Start โ€” 3 Steps

Step 1: Define a Catalog

import { defineCatalog } from '@json-render/core'
import { z } from 'zod'

export const catalog = defineCatalog({
  Card: {
    props: z.object({
      title: z.string(),
      description: z.string().optional(),
    }),
    children: true,
  },
  Button: {
    props: z.object({
      label: z.string(),
      variant: z.enum(['default', 'destructive', 'outline', 'ghost']),
    }),
    children: false,
  },
  StatGrid: {
    props: z.object({
      items: z.array(z.object({
        label: z.string(),
        value: z.string(),
        trend: z.enum(['up', 'down', 'flat']).optional(),
      })).max(20),
    }),
    children: false,
  },
})

LLM Structured Output Compatibility

Use jsonSchema(\{ strict: true \}) to export catalog schemas compatible with LLM structured output APIs (OpenAI, Anthropic, Gemini):

import { jsonSchema } from '@json-render/core'

const schema = jsonSchema(catalog, { strict: true })
// Pass to OpenAI response_format, Anthropic tool_use, or Gemini structured output

Step 2: Implement Components

import type { CatalogComponents } from '@json-render/react'
import type { catalog } from './catalog'

export const components: CatalogComponents<typeof catalog> = {
  Card: ({ title, description, children }) => (
    <div className="rounded-lg border p-4">
      <h3 className="font-semibold">{title}</h3>
      {description && <p className="text-muted-foreground">{description}</p>}
      {children}
    </div>
  ),
  Button: ({ label, variant }) => (
    <button className={cn('btn', `btn-${variant}`)}>{label}</button>
  ),
  StatGrid: ({ items }) => (
    <div className="grid grid-cols-3 gap-4">
      {items.map((item) => (
        <div key={item.label}>
          <span>{item.label}</span>
          <strong>{item.value}</strong>
        </div>
      ))}
    </div>
  ),
}

Step 3: Render a Spec

import { Render } from '@json-render/react'
import { catalog } from './catalog'
import { components } from './components'

function App({ spec }: { spec: JsonRenderSpec }) {
  return <Render catalog={catalog} components={components} spec={spec} />
}

Spec Format

The JSON spec is a flat tree โ€” no nesting, just IDs and references. Load references/spec-format.md for full documentation.

{
  "root": "card-1",
  "elements": {
    "card-1": {
      "type": "Card",
      "props": { "title": "Dashboard" },
      "children": ["chart-1", "btn-1"]
    },
    "btn-1": {
      "type": "Button",
      "props": { "label": "View Details", "variant": "default" }
    }
  }
}

With Interactivity (on / watch / state)

{
  "root": "card-1",
  "elements": {
    "card-1": {
      "type": "Card",
      "props": { "title": "Dashboard" },
      "children": ["chart-1", "btn-1"],
      "on": { "press": { "action": "setState", "path": "/view", "value": "detail" } },
      "watch": { "/data": { "action": "load_data", "url": "/api/stats" } }
    }
  },
  "state": { "/activeTab": "overview" }
}

Load rules/action-state.md for event handlers, watch bindings, and state adapter patterns.

YAML Mode โ€” 30% Fewer Tokens

For standalone (non-streaming) generation, YAML specs use ~30% fewer tokens than JSON:

root: card-1
elements:
  card-1:
    type: Card
    props:
      title: Dashboard
    children: [chart-1, btn-1]
  btn-1:
    type: Button
    props:
      label: View Details
      variant: default

Use JSON for inline mode / streaming (JSON Patch RFC 6902 over JSONL requires JSON). Use YAML for standalone mode where token cost matters. Load rules/token-optimization.md for selection criteria.

Progressive Streaming

json-render supports progressive rendering during streaming. As the AI generates spec elements, they render immediately โ€” the user sees the UI building in real-time. This uses JSON Patch (RFC 6902) operations streamed over JSONL:

{"op":"add","path":"/elements/card-1","value":{"type":"Card","props":{"title":"Dashboard"},"children":[]}}
{"op":"add","path":"/elements/btn-1","value":{"type":"Button","props":{"label":"Save","variant":"default"}}}
{"op":"add","path":"/elements/card-1/children/-","value":"btn-1"}

Elements render as soon as their props are complete โ€” no waiting for the full spec.

@json-render/shadcn โ€” 36 Pre-Built Components

The @json-render/shadcn package provides a production-ready catalog of 36 components with Zod schemas already defined. Load rules/shadcn-catalog.md for the full component list and when to extend vs use as-is.

Svelte: @json-render/shadcn-svelte (added in 0.16) mirrors the same 36 components for Svelte 5 + Tailwind projects.

import { shadcnCatalog, shadcnComponents } from '@json-render/shadcn'
import { mergeCatalogs } from '@json-render/core'

// Use as-is
<Render catalog={shadcnCatalog} components={shadcnComponents} spec={spec} />

// Or merge with custom components
const catalog = mergeCatalogs(shadcnCatalog, customCatalog)

Style-Aware Catalogs

The shadcn catalog components use default Tailwind classes. When your project uses a specific shadcn v4 style (Luma, Nova, etc.), override component implementations to match:

import { shadcnCatalog, shadcnComponents } from '@json-render/shadcn'
import { mergeCatalogs, type CatalogComponents } from '@json-render/core'

// Override shadcn component implementations for Luma style
const lumaComponents: Partial<CatalogComponents<typeof shadcnCatalog>> = {
  Card: ({ title, description, children }) => (
    <div className="rounded-4xl border shadow-md ring-1 ring-foreground/5 p-6">
      <h3 className="font-semibold">{title}</h3>
      {description && <p className="text-muted-foreground">{description}</p>}
      <div className="mt-6">{children}</div>
    </div>
  ),
  Button: ({ label, variant }) => (
    <button className={cn('rounded-4xl', buttonVariants({ variant }))}>{label}</button>
  ),
}

// Merge: catalog schema unchanged, only rendering adapts to style
const components = { ...shadcnComponents, ...lumaComponents }

Detection pattern: Read components.json โ†’ "style" field to determine which overrides to apply. Style-specific class names: Luma (rounded-4xl, shadow-md, gap-6), Nova (compact px-2 py-1), Lyra (rounded-none).

Edit Modes โ€” patch / merge / diff (0.14+)

For updating specs after initial render (AI-driven refinements, user edits, partial regenerations), core ships three universal edit modes:

ModeSpecWhen to use
patchRFC 6902 JSON PatchPrecise, streamed diffs (already used for progressive streaming)
mergeRFC 7396 JSON Merge PatchSimpler updates, whole-field replacements
diffUnified diff of serialized specAI-native output when the model prefers plaintext diffs
import { deepMergeSpec, diffToPatches, buildEditUserPrompt } from '@json-render/core'

// Ask the model for an edit in whichever format it finds easiest
const prompt = buildEditUserPrompt(currentSpec, instruction, { format: 'yaml', mode: 'merge' })

// Normalize any edit mode to RFC 6902 patches for application
const patches = diffToPatches(aiResponse)
const next = deepMergeSpec(currentSpec, patches)

buildUserPrompt() also gained format and serializer options in 0.14 โ€” pick YAML for standalone specs and JSON for streaming.

Package Ecosystem

Core + 23 renderer/integration packages covering web, mobile, terminal, 3D, codegen, and state management. Load references/package-ecosystem.md for the full list organized by category.

Added since 0.13:

  • @json-render/yaml (0.14) โ€” YAML wire format + streaming parser
  • @json-render/ink (0.15) โ€” terminal UI renderer (Ink-based, 20+ components)
  • @json-render/next (0.16) โ€” generates full Next.js apps (routes, layouts, SSR, metadata)
  • @json-render/shadcn-svelte (0.16) โ€” 36-component Svelte 5 mirror of the React shadcn catalog
  • @json-render/react-three-fiber now ships 20 components (includes GaussianSplat in 0.17)

When to Use vs When NOT to Use

Use json-render when:

  • AI generates UI and you need to constrain what it can produce
  • You want runtime-validated specs that prevent hallucinated components
  • You need cross-platform rendering (React, Vue, Svelte, React Native, PDF, email)
  • You are building generative UI features (dashboards, reports, forms from natural language)

Do NOT use json-render when:

  • Building static, developer-authored UI โ€” use components directly
  • AI generates code (JSX/TSX) rather than specs โ€” use standard code generation
  • You need full creative freedom without catalog constraints โ€” json-render is deliberately restrictive
  • Performance-critical rendering with thousands of elements โ€” the flat-tree abstraction adds overhead

Migrating from Custom GenUI

If you have existing custom generative UI (hand-rolled JSON-to-component mapping), load references/migration-from-genui.md for a step-by-step migration guide.

Rule Details

Catalog Definition

How to define catalogs with defineCatalog() and Zod schemas.

RuleFileKey Pattern
Catalog Definitionrules/catalog-definition.mddefineCatalog with Zod schemas, children types

Prop Constraints

Constraining props to prevent AI hallucination.

RuleFileKey Pattern
Prop Constraintsrules/prop-constraints.mdz.enum, z.string().max(), z.array().max()

shadcn Catalog

Using the 36 pre-built shadcn components.

RuleFileKey Pattern
shadcn Catalogrules/shadcn-catalog.md@json-render/shadcn components and extension

Token Optimization

Choosing JSON vs YAML for token efficiency.

RuleFileKey Pattern
Token Optimizationrules/token-optimization.mdYAML for standalone mode, JSON for inline/streaming

Actions & State

Adding interactivity with events, watchers, and state.

RuleFileKey Pattern
Action & Staterules/action-state.mdon events, watch reactivity, state adapters

Key Decisions

DecisionRecommendation
Custom vs shadcn catalogStart with shadcn, extend with custom types for domain-specific components
JSON vs YAML spec formatYAML for standalone mode (30% fewer tokens), JSON for inline/streaming
Zod constraint strictnessTighter is better โ€” use z.enum over z.string, z.array().max() over unbounded
State management adapterMatch your app's existing state library (Zustand, Redux, Jotai, XState)

Common Mistakes

  1. Using z.any() or z.unknown() in catalog props โ€” defeats the purpose of catalog constraints, AI can generate anything
  2. Always using JSON specs โ€” wastes 30% tokens when inline/streaming is not needed (use YAML in standalone mode)
  3. Nesting component definitions โ€” json-render uses a flat tree; all elements are siblings referenced by ID
  4. Skipping mergeCatalogs() when combining shadcn + custom โ€” manual merging loses type safety
  5. Not setting .max() on arrays โ€” AI can generate unbounded lists that break layouts
  • ork:ai-ui-generation โ€” AI-assisted UI generation patterns for v0, Bolt, Cursor
  • ork:ui-components โ€” shadcn/ui component patterns and CVA variants
  • ork:component-search โ€” Finding and evaluating React/Vue components
  • ork:design-to-code โ€” Converting designs to production code

Rules (5)

Actions and State Bindings in Specs โ€” MEDIUM

Actions and State Bindings in Specs

json-render specs support three interactivity primitives: on (event handlers), watch (reactive data bindings), and state (shared state tree). These turn static specs into interactive UIs without AI generating imperative code.

Incorrect:

{
  "root": "form-1",
  "elements": {
    "form-1": {
      "type": "Card",
      "props": { "title": "Settings" },
      "children": ["btn-1"]
    },
    "btn-1": {
      "type": "Button",
      "props": { "label": "Save" }
    }
  }
}

No interactivity โ€” button click does nothing, no state, no data loading.

Correct:

{
  "root": "form-1",
  "elements": {
    "form-1": {
      "type": "Card",
      "props": { "title": "Settings" },
      "children": ["toggle-1", "btn-1"],
      "watch": {
        "/settings": {
          "action": "load_data",
          "url": "/api/settings"
        }
      }
    },
    "toggle-1": {
      "type": "Switch",
      "props": { "label": "Dark Mode", "checked": false },
      "on": {
        "change": {
          "action": "setState",
          "path": "/settings/darkMode",
          "value": "toggle"
        }
      }
    },
    "btn-1": {
      "type": "Button",
      "props": { "label": "Save", "variant": "default" },
      "on": {
        "press": {
          "action": "submit",
          "url": "/api/settings",
          "method": "POST",
          "body": { "$ref": "/settings" }
        }
      }
    }
  },
  "state": {
    "/settings": {
      "darkMode": false,
      "notifications": true
    }
  }
}

The Three Primitives

on โ€” Event Handlers: Attached to elements, fire on user interaction.

EventTriggers When
pressButton click / tap
changeInput, Select, Switch, Checkbox value change
submitForm submission
focus / blurElement focus state changes

watch โ€” Reactive Data Bindings: Attached to elements, react to state path changes. Supports action, url, and optional interval (polling in ms).

"watch": { "/data/stats": { "action": "load_data", "url": "/api/stats", "interval": 30000 } }

state โ€” Shared State Tree: Top-level object with JSON Pointer paths. All setState and watch bindings reference these paths.

"state": { "/activeTab": "overview", "/filters": { "status": "all" }, "/data": null }

Built-in Actions

ActionDescriptionExample
setStateSet a state path to a value\{ "action": "setState", "path": "/tab", "value": "settings" \}
load_dataFetch data from URL into state\{ "action": "load_data", "url": "/api/data" \}
submitPOST/PUT data to URL\{ "action": "submit", "url": "/api/save", "method": "POST" \}
navigateClient-side navigation\{ "action": "navigate", "to": "/dashboard" \}
toggleToggle boolean state path\{ "action": "setState", "path": "/open", "value": "toggle" \}

State Adapters

Connect json-render state to your app's state management:

// Zustand adapter
import { createZustandAdapter } from '@json-render/zustand'
const adapter = createZustandAdapter(useAppStore)
<Render catalog={catalog} components={components} spec={spec} stateAdapter={adapter} />

// Redux adapter
import { createReduxAdapter } from '@json-render/redux'
const adapter = createReduxAdapter(store)

// Jotai adapter
import { createJotaiAdapter } from '@json-render/jotai'
const adapter = createJotaiAdapter()

// XState adapter
import { createXStateAdapter } from '@json-render/xstate'
const adapter = createXStateAdapter(machine)

Key rules:

  • Use on for user-initiated actions (clicks, input changes) โ€” never for data loading
  • Use watch for reactive data fetching โ€” it re-fetches when the watched state path changes
  • Define all state paths in the top-level state object โ€” referencing undefined paths is a runtime error
  • Use $ref in action bodies to reference state paths: "body": \{ "$ref": "/formData" \}
  • Choose the state adapter that matches your existing state management library โ€” do not mix adapters

Reference: https://github.com/nicholasgriffintn/json-render

Catalog Definition with defineCatalog and Zod โ€” HIGH

Catalog Definition with defineCatalog and Zod

Every json-render project starts with a catalog โ€” a Zod-typed registry of components the AI is allowed to generate. The catalog is the contract: if a type is not in the catalog, it cannot appear in specs.

Incorrect:

// No type safety โ€” AI can generate anything, props are unchecked
const components = {
  Card: ({ title, children }) => <div>{title}{children}</div>,
  Button: ({ label }) => <button>{label}</button>,
}

// Rendering without a catalog โ€” no validation
function App({ spec }) {
  return <DynamicRenderer components={components} spec={spec} />
}

Correct:

import { defineCatalog } from '@json-render/core'
import { z } from 'zod'

export const catalog = defineCatalog({
  Card: {
    // props: Zod schema validates every prop AI generates
    props: z.object({
      title: z.string().max(100),
      description: z.string().max(500).optional(),
      elevated: z.boolean().default(false),
    }),
    // children: true = accepts child elements, false = leaf node
    children: true,
  },
  Button: {
    props: z.object({
      label: z.string().max(50),
      variant: z.enum(['default', 'destructive', 'outline', 'ghost']),
      size: z.enum(['sm', 'md', 'lg']).default('md'),
      disabled: z.boolean().default(false),
    }),
    children: false,
  },
  DataTable: {
    props: z.object({
      columns: z.array(z.object({
        key: z.string(),
        label: z.string(),
        sortable: z.boolean().default(false),
      })).min(1).max(12),
      rows: z.array(z.record(z.string())).max(100),
    }),
    children: false,
  },
})

// Type-safe rendering with catalog validation
import { Render } from '@json-render/react'
<Render catalog={catalog} components={components} spec={spec} />

Children Types

ValueMeaningUse For
trueAccepts any catalog childrenLayout components (Card, Section, Grid)
falseLeaf node, no childrenData display (StatGrid, Chart, Badge)
['Button', 'Badge']Only accepts specific types as childrenConstrained containers (Toolbar accepts only Button)
Toolbar: {
  props: z.object({ orientation: z.enum(['horizontal', 'vertical']) }),
  children: ['Button', 'Badge'],  // Only Button and Badge allowed as children
},

Merging Catalogs

Use mergeCatalogs() to combine the shadcn base with custom domain components:

import { mergeCatalogs } from '@json-render/core'
import { shadcnCatalog } from '@json-render/shadcn'

const appCatalog = mergeCatalogs(shadcnCatalog, {
  PricingCard: {
    props: z.object({
      plan: z.enum(['free', 'pro', 'enterprise']),
      price: z.string(),
      features: z.array(z.string()).max(10),
    }),
    children: false,
  },
})

Key rules:

  • Every component in the catalog must have a props Zod schema and a children declaration
  • Use .max(), .min(), and .default() on all schemas to bound what AI can generate
  • Use typed children arrays (['Button']) for containers that should only accept specific child types
  • Use mergeCatalogs() to combine catalogs โ€” manual spreading loses runtime validation
  • Export the catalog type for use in component implementations: type AppCatalog = typeof catalog

Reference: https://github.com/nicholasgriffintn/json-render

Prop Constraints for AI Safety โ€” HIGH

Prop Constraints for AI Safety

Catalog props are the primary defense against AI hallucination. Every prop should be as tightly constrained as possible โ€” bounded strings, explicit enums, capped arrays. The tighter the constraints, the more predictable and safe the AI output.

Incorrect:

// z.any() defeats the entire purpose of the catalog
BadComponent: {
  props: z.object({
    data: z.any(),                    // AI can put anything here
    items: z.array(z.unknown()),      // Unbounded, untyped list
    color: z.string(),                // AI hallucinates hex codes, CSS names, anything
    content: z.string(),              // No length limit โ€” AI can generate 10K chars
    config: z.record(z.any()),        // Open-ended object
  }),
  children: true,
},

Correct:

GoodComponent: {
  props: z.object({
    // z.enum bounds choices to known-safe values
    variant: z.enum(['primary', 'secondary', 'destructive']),
    size: z.enum(['sm', 'md', 'lg']),
    status: z.enum(['active', 'inactive', 'pending']),

    // z.string().max() prevents unbounded text
    title: z.string().min(1).max(100),
    description: z.string().max(500).optional(),

    // z.array().max() caps list length to prevent layout overflow
    items: z.array(z.object({
      label: z.string().max(50),
      value: z.string().max(100),
    })).min(1).max(20),

    // z.number() with range for numeric props
    columns: z.number().int().min(1).max(6),
    progress: z.number().min(0).max(100),

    // z.boolean() with default for optional flags
    disabled: z.boolean().default(false),
    loading: z.boolean().default(false),
  }),
  children: false,
},

Constraint Patterns by Prop Type

Prop TypeWeak (avoid)Strong (use)
Text contentz.string()z.string().min(1).max(200)
Color/variantz.string()z.enum(['primary', 'secondary'])
List itemsz.array(z.any())z.array(schema).min(1).max(20)
Numericz.number()z.number().int().min(0).max(100)
Boolean flags(no constraint)z.boolean().default(false)
Freeform objectz.record(z.any())z.object(\{ specific: z.string() \})
URL/imagez.string()z.string().url().max(2048)

Refinements for Complex Validation

DateRange: {
  props: z.object({
    start: z.string().date(),
    end: z.string().date(),
  }).refine(
    (data) => new Date(data.end) > new Date(data.start),
    { message: 'end must be after start' }
  ),
  children: false,
},

Key rules:

  • Never use z.any(), z.unknown(), or bare z.record() in catalog props โ€” these bypass AI safety
  • Always set .max() on strings and arrays to prevent unbounded generation
  • Use z.enum() instead of z.string() whenever possible โ€” enums constrain AI to valid values
  • Add .default() to optional boolean and enum props โ€” prevents undefined gaps in rendered output
  • Use .refine() for cross-field validation (date ranges, conditional requirements)
  • Test constraints by checking: "Can AI generate a value that would break my UI?" If yes, tighten the schema

Reference: https://zod.dev

shadcn Catalog โ€” 29 Pre-Built Components โ€” MEDIUM

shadcn Catalog โ€” 29 Pre-Built Components

@json-render/shadcn provides 29 components with Zod schemas and implementations ready to use. Start here before building custom catalog entries โ€” these components cover most dashboard, form, and content display needs.

Incorrect:

// Building from scratch when shadcn already provides it
import { defineCatalog } from '@json-render/core'
import { z } from 'zod'

const catalog = defineCatalog({
  // Reimplementing what shadcn already has
  Alert: {
    props: z.object({
      title: z.string(),
      description: z.string(),
      variant: z.enum(['default', 'destructive']),
    }),
    children: false,
  },
  // ... 35 more hand-rolled components
})

Correct:

import { shadcnCatalog, shadcnComponents } from '@json-render/shadcn'
import { mergeCatalogs } from '@json-render/core'
import { z } from 'zod'

// Use shadcn as-is for standard components
<Render catalog={shadcnCatalog} components={shadcnComponents} spec={spec} />

// Extend with domain-specific components only
const appCatalog = mergeCatalogs(shadcnCatalog, {
  PricingCard: {
    props: z.object({
      plan: z.enum(['free', 'pro', 'enterprise']),
      price: z.string().max(20),
      features: z.array(z.string().max(100)).max(10),
    }),
    children: false,
  },
})

The 29 shadcn Components

Layout & Container:

ComponentKey PropsChildren
Cardtitle, description, footertrue
Accordiontype (single/multiple), collapsibletrue
TabsdefaultValue, orientationtrue
Sheetside (top/right/bottom/left)true
Dialogtitle, descriptiontrue
Collapsibleopen, defaultOpentrue
Separatororientation, decorativefalse
ScrollAreaorientationtrue
AspectRatioratiotrue

Data Display:

ComponentKey PropsChildren
Tablecolumns, rows, captionfalse
Badgevariant, labelfalse
Avatarsrc, alt, fallbackfalse
Progressvalue, maxfalse
Skeletonwidth, height, variantfalse
HoverCardtriggerTexttrue
Tooltipcontent, sidetrue

Form & Input:

ComponentKey PropsChildren
Buttonlabel, variant, size, disabledfalse
Inputplaceholder, type, labelfalse
Textareaplaceholder, rows, labelfalse
Selectoptions, placeholder, labelfalse
Checkboxlabel, checked, disabledfalse
RadioGroupoptions, defaultValue, labelfalse
Switchlabel, checked, disabledfalse
Slidermin, max, step, defaultValuefalse
Labeltext, htmlForfalse
Togglelabel, pressed, variantfalse
ToggleGrouptype, itemsfalse

Navigation:

ComponentKey PropsChildren
NavigationMenuitemsfalse
Breadcrumbitems, separatorfalse
Menubarmenusfalse
DropdownMenutriggerLabel, itemsfalse
ContextMenuitemstrue
Commandplaceholder, groupsfalse

Feedback:

ComponentKey PropsChildren
Alerttitle, description, variantfalse
AlertDialogtitle, description, actionLabelfalse
Toasttitle, description, variant, actionfalse

When to Extend vs Use As-Is

ScenarioApproach
Standard UI (dashboards, settings, forms)Use shadcn as-is
Domain-specific display (pricing, metrics, timelines)Add custom components via mergeCatalogs
Branded components (custom design system)Override shadcn implementations, keep schemas
Project uses shadcn v4 style (Luma, Nova, etc.)Override implementations with style-correct classes
Highly specialized (3D, charts, maps)Add custom + use @json-render/react-three-fiber

Style-Aware Overrides

When your project uses a shadcn v4 style, the default shadcnComponents use generic Tailwind classes that may not match. Override implementations to match the project's style.

Incorrect โ€” using default components in a Luma project:

// Default shadcnComponents use rounded-lg โ€” wrong for Luma (rounded-4xl)
<Render catalog={shadcnCatalog} components={shadcnComponents} spec={spec} />

Correct โ€” overriding implementations for project's v4 style:

import { shadcnCatalog, shadcnComponents } from '@json-render/shadcn'
// Read components.json โ†’ style to determine overrides (Luma, Nova, etc.)
const lumaOverrides = {
  Card: ({ title, children }) => (
    <div className="rounded-4xl border shadow-md ring-1 ring-foreground/5 p-6">
      <h3 className="font-semibold">{title}</h3>
      <div className="mt-6">{children}</div>
    </div>
  ),
}
const components = { ...shadcnComponents, ...lumaOverrides }

Key rules:

  • Start with shadcnCatalog โ€” covers 80% of common UI patterns out of the box
  • Use mergeCatalogs() to add domain-specific components alongside shadcn
  • Override implementations (not schemas) for branded styling or v4 style conformance
  • Check components.json โ†’ "style" to determine which class overrides apply

Token Optimization โ€” YAML Mode โ€” MEDIUM

Token Optimization โ€” YAML Mode

json-render supports both JSON and YAML spec formats. YAML uses ~30% fewer tokens than equivalent JSON because it eliminates braces, brackets, quotes around keys, and trailing commas. For standalone mode (formerly "generate"), YAML is the default choice. Note: "generate"/"chat" mode names were deprecated in v0.12.1 โ€” use "standalone"/"inline" instead.

Incorrect:

// Always using JSON regardless of use case โ€” wastes tokens
const systemPrompt = `Generate a json-render spec in JSON format:
{
  "root": "card-1",
  "elements": {
    "card-1": {
      "type": "Card",
      "props": {
        "title": "Welcome",
        "description": "Getting started guide"
      },
      "children": ["btn-1", "btn-2"]
    },
    "btn-1": {
      "type": "Button",
      "props": {
        "label": "Continue",
        "variant": "default"
      }
    },
    "btn-2": {
      "type": "Button",
      "props": {
        "label": "Skip",
        "variant": "ghost"
      }
    }
  }
}`
// ~180 tokens for syntax overhead

Correct:

// YAML for standalone mode โ€” 30% fewer tokens
import { parseYamlSpec } from '@json-render/yaml'

const systemPrompt = `Generate a json-render spec in YAML format:
root: card-1
elements:
  card-1:
    type: Card
    props:
      title: Welcome
      description: Getting started guide
    children: [btn-1, btn-2]
  btn-1:
    type: Button
    props:
      label: Continue
      variant: default
  btn-2:
    type: Button
    props:
      label: Skip
      variant: ghost`

// Parse YAML spec before rendering
const spec = parseYamlSpec(yamlString)
<Render catalog={catalog} components={components} spec={spec} />

Format Selection Criteria

CriterionJSONYAML
Inline mode / streaming (progressive render)RequiredNot supported
Standalone modeWorks but wasteful30% fewer tokens
Token cost sensitivityHigherLower
Parsing reliabilityNative JSON.parseRequires yaml parser
AI familiarityHigher (more training data)High (common in configs)
Spec debuggingEasy (structured)Easy (readable)

Decision Rule

If inline mode / streaming (progressive render needed) โ†’ JSON
If standalone AND token cost matters โ†’ YAML
If standalone AND debugging matters โ†’ either (both readable)
Default for standalone โ†’ YAML

Token Comparison Example

A spec with 5 components:

  • JSON: ~450 tokens
  • YAML: ~310 tokens
  • Savings: ~140 tokens (31%)

At scale (100 specs/day, $3/M input tokens with Haiku): ~$0.04/day savings. The real value is in output token reduction โ€” LLMs generate fewer tokens in YAML format, which reduces latency.

Key rules:

  • Use YAML for standalone mode โ€” it reduces both input and output tokens by ~30%
  • Use JSON for inline mode / streaming โ€” JSON Patch (RFC 6902) operates on JSON, not YAML
  • Import parseYamlSpec from @json-render/yaml to convert YAML strings to spec objects
  • Do not mix formats in a single spec โ€” pick one and stay consistent
  • Measure token usage with your provider's tokenizer to validate savings for your specific catalogs

Reference: https://github.com/nicholasgriffintn/json-render


References (25)

Migrating from Custom GenUI to json-render

Migrating from Custom GenUI to json-render

Guide for migrating hand-rolled generative UI systems (custom JSON-to-component mapping) to json-render catalogs. The migration adds Zod validation, streaming, and cross-platform support without rewriting component implementations.

Common Custom GenUI Patterns

Most custom GenUI systems follow one of these patterns:

Pattern A: Direct Type Mapping

// Custom: components map keyed by string type
const componentMap = {
  'card': CardComponent,
  'button': ButtonComponent,
  'table': TableComponent,
}

function render(spec) {
  const Component = componentMap[spec.type]
  return <Component {...spec.props}>{spec.children?.map(render)}</Component>
}

Pattern B: Switch Statement

function renderElement(element) {
  switch (element.type) {
    case 'card': return <Card {...element.props} />
    case 'button': return <Button {...element.props} />
    default: return null // Silent failure on unknown types
  }
}

Pattern C: Nested JSON

{
  "type": "card",
  "props": { "title": "Dashboard" },
  "children": [
    {
      "type": "button",
      "props": { "label": "Save" },
      "children": []
    }
  ]
}

Migration Steps

Step 1: Inventory Existing Components

List every component type your current system supports, along with the props each accepts:

// Audit your component map / switch statement
const inventory = [
  { type: 'card', props: ['title', 'description', 'elevated'], hasChildren: true },
  { type: 'button', props: ['label', 'variant', 'onClick'], hasChildren: false },
  { type: 'table', props: ['columns', 'data', 'sortable'], hasChildren: false },
]

Step 2: Define Zod Schemas for Each Component

Convert loosely-typed props to Zod schemas:

// Before: untyped props โ€” AI can pass anything
{ type: 'card', props: { title: 'anything', extra: 'unknown-prop' } }

// After: Zod-constrained catalog
import { defineCatalog } from '@json-render/core'
import { z } from 'zod'

export const catalog = defineCatalog({
  Card: {
    props: z.object({
      title: z.string().max(100),
      description: z.string().max(500).optional(),
      elevated: z.boolean().default(false),
    }),
    children: true,
  },
  Button: {
    props: z.object({
      label: z.string().max(50),
      variant: z.enum(['default', 'destructive', 'outline', 'ghost']),
      // Note: onClick is NOT in the catalog โ€” use on.press instead
    }),
    children: false,
  },
})

Step 3: Flatten Nested Specs

Convert nested JSON to flat-tree format:

// Before: nested
{
  "type": "card",
  "props": { "title": "Dashboard" },
  "children": [
    { "type": "button", "props": { "label": "Save" } }
  ]
}

// After: flat tree
{
  "root": "card-1",
  "elements": {
    "card-1": {
      "type": "Card",
      "props": { "title": "Dashboard" },
      "children": ["btn-1"]
    },
    "btn-1": {
      "type": "Button",
      "props": { "label": "Save", "variant": "default" }
    }
  }
}

Step 4: Replace Event Handlers

Custom GenUI often passes onClick, onChange as props. json-render uses the on field instead:

// Before: handler in props
{ "type": "button", "props": { "label": "Save", "onClick": "save()" } }

// After: on field with action
{
  "type": "Button",
  "props": { "label": "Save", "variant": "default" },
  "on": { "press": { "action": "submit", "url": "/api/save", "method": "POST" } }
}

Step 5: Wrap Existing Component Implementations

Your existing React/Vue components work as json-render implementations with minimal changes:

import type { CatalogComponents } from '@json-render/react'
import type { catalog } from './catalog'

// Reuse existing component implementations
import { Card as ExistingCard } from '@/components/Card'
import { Button as ExistingButton } from '@/components/Button'

export const components: CatalogComponents<typeof catalog> = {
  Card: ({ title, description, elevated, children }) => (
    <ExistingCard title={title} description={description} elevated={elevated}>
      {children}
    </ExistingCard>
  ),
  Button: ({ label, variant }) => (
    <ExistingButton variant={variant}>{label}</ExistingButton>
  ),
}

Step 6: Update AI Prompts

Update your LLM system prompts to generate flat-tree specs instead of nested JSON:

// Before: "Generate a UI component tree as nested JSON"
// After: "Generate a json-render spec. Use the flat-tree format with root and elements."

Include the catalog schema in the system prompt so the AI knows which types and props are available.

Migration Checklist

  • Inventoried all existing component types and props
  • Created Zod schemas for each component with proper constraints
  • Converted nested specs to flat-tree format
  • Replaced event handler props with on field actions
  • Wrapped existing component implementations with catalog types
  • Updated AI system prompts to generate flat-tree specs
  • Added runtime validation via &lt;Render catalog=\{...\}&gt; component
  • Tested with existing specs to verify backward compatibility
  • Enabled streaming support if using progressive rendering

json-render Package Ecosystem

json-render Package Ecosystem

23 packages under the @json-render scope, organized by category. All packages share the same spec format โ€” a spec generated for React works with Vue, Svelte, React Native, PDF, and email renderers.

Foundation

PackagePurpose
@json-render/coredefineCatalog(), mergeCatalogs(), spec validation, type utilities

Web Renderers

PackageFrameworkNotes
@json-render/reactReact 18/19&lt;Render&gt; component, hooks, streaming support
@json-render/vueVue 3&lt;Render&gt; component, composables
@json-render/svelteSvelte 5&lt;Render&gt; component, runes-compatible
@json-render/solidSolidJS&lt;Render&gt; component, fine-grained reactivity

Component Libraries

PackageComponentsNotes
@json-render/shadcn29shadcn/ui components with Zod schemas and implementations

Mobile

PackagePlatformNotes
@json-render/react-nativeiOS / Android25+ components, Expo and bare RN support

Output Renderers

PackageOutputNotes
@json-render/react-pdfPDFGenerates PDF documents from specs via react-pdf
@json-render/react-emailEmail HTMLEmail-safe HTML from specs via react-email
@json-render/imagePNG / SVGRenders specs to images via Satori
@json-render/remotionVideoAnimated specs rendered as video via Remotion
@json-render/yamlYAML specsParse/stringify YAML format specs (30% fewer tokens)

3D

PackagePurposeNotes
@json-render/react-three-fiber3D scenesWebGL rendering via React Three Fiber

MCP (Model Context Protocol)

PackagePurposeNotes
@json-render/mcpMCP tool outputRender specs as MCP tool results for AI agents

Code Generation

PackagePurposeNotes
@json-render/codegenJSX/TSX outputConvert specs to static React/Vue/Svelte component code

State Adapters

PackageLibraryNotes
@json-render/reduxRedux ToolkitBidirectional state sync with Redux store
@json-render/zustandZustandAdapter for Zustand stores
@json-render/jotaiJotaiAtom-based state adapter
@json-render/xstateXState 5State machine adapter for complex workflows

Installation Patterns

Minimal (React + custom catalog):

npm install @json-render/core @json-render/react zod

With shadcn components:

npm install @json-render/core @json-render/react @json-render/shadcn zod

Cross-platform (web + mobile + PDF):

npm install @json-render/core @json-render/react @json-render/react-native @json-render/react-pdf zod

With YAML optimization:

npm install @json-render/core @json-render/react @json-render/yaml zod

With state management (Zustand example):

npm install @json-render/core @json-render/react @json-render/zustand zod zustand

Write Once, Render Anywhere

The key value proposition: a single spec works across all renderers. Generate a dashboard spec once, render it as:

  • Interactive web UI (@json-render/react)
  • Mobile app (@json-render/react-native)
  • PDF report (@json-render/react-pdf)
  • Email digest (@json-render/react-email)
  • Static image (@json-render/image)

The catalog + spec is the shared contract. Each renderer maps catalog types to platform-specific implementations.

json-render Spec Format

json-render Spec Format

The JSON spec is the data contract between AI and the renderer. It uses a flat-tree structure where all elements are top-level entries referenced by ID โ€” no nesting.

Structure

{
  "root": "<element-id>",
  "elements": {
    "<element-id>": {
      "type": "<CatalogComponentName>",
      "props": { ... },
      "children": ["<child-id-1>", "<child-id-2>"],
      "on": { "<event>": { "action": "...", ... } },
      "watch": { "<state-path>": { "action": "...", ... } }
    }
  },
  "state": {
    "/<path>": <value>
  }
}

Fields

root (required)

The ID of the top-level element to render. Must reference a key in elements.

{ "root": "page-container" }

elements (required)

A flat map of element ID to element definition. Every element in the spec lives here โ€” no nesting.

Element Fields

type (required): Must match a component name in the catalog. Runtime validation rejects unknown types.

props (required): Object matching the Zod schema defined in the catalog for this type. Validated at render time.

children (optional): Array of element IDs that are children of this element. Only valid if the catalog entry allows children (children: true or a typed array like ['Button']).

"children": ["heading-1", "content-1", "footer-btn"]

on (optional): Event handlers keyed by event name.

"on": {
  "press": { "action": "setState", "path": "/view", "value": "detail" },
  "change": { "action": "setState", "path": "/search", "value": "$event" }
}

The $event variable refers to the event value (input text, checkbox state, etc.).

watch (optional): Reactive bindings keyed by state path. When the watched path changes, the action fires.

"watch": {
  "/filters": {
    "action": "load_data",
    "url": "/api/items",
    "params": { "$ref": "/filters" }
  }
}

state (optional)

Top-level state tree using JSON Pointer paths. All setState and watch references point to paths in this tree.

"state": {
  "/activeTab": "overview",
  "/selectedIds": [],
  "/filters": { "status": "all" },
  "/data": null
}

Flat-Tree Design

json-render uses a flat tree (all elements as siblings) rather than nested JSON because:

  1. Streaming โ€” Elements can be added independently via JSON Patch without re-sending parent context
  2. Reuse โ€” The same element ID can be referenced as a child of multiple parents
  3. Simplicity โ€” AI generates a flat list, not deeply nested structures that are harder to validate
  4. Patching โ€” Updating a single element requires one patch operation, not a deep path traversal

Complete Example

{
  "root": "dashboard",
  "elements": {
    "dashboard": {
      "type": "Card",
      "props": { "title": "Sales Dashboard" },
      "children": ["stats", "tabs"]
    },
    "stats": {
      "type": "StatGrid",
      "props": {
        "items": [
          { "label": "Revenue", "value": "$42K", "trend": "up" },
          { "label": "Orders", "value": "1,234", "trend": "up" },
          { "label": "Refunds", "value": "23", "trend": "down" }
        ]
      }
    },
    "tabs": {
      "type": "Tabs",
      "props": { "defaultValue": "chart" },
      "children": ["chart-tab", "table-tab"]
    },
    "chart-tab": {
      "type": "Card",
      "props": { "title": "Revenue Chart" },
      "children": []
    },
    "table-tab": {
      "type": "Table",
      "props": {
        "columns": [
          { "key": "date", "label": "Date", "sortable": true },
          { "key": "amount", "label": "Amount", "sortable": true },
          { "key": "status", "label": "Status", "sortable": false }
        ],
        "rows": []
      },
      "watch": {
        "/data/orders": {
          "action": "load_data",
          "url": "/api/orders"
        }
      }
    }
  },
  "state": {
    "/data/orders": null
  }
}

Validation Flow

  1. root must reference an existing element ID
  2. Each element type must exist in the catalog
  3. Each element props must pass the Zod schema for that type
  4. Each children entry must reference an existing element ID
  5. Children are only allowed if the catalog entry permits them
  6. on event names must be valid for the component type
  7. watch paths must reference paths in the state tree

Storybook โ†’ json-render catalog import โ€” HIGH

Storybook โ†’ json-render Catalog Import

The storybook-to-catalog.mjs script imports a Storybook component manifest (from @storybook/addon-mcp's list-all-documentation tool) and emits a Zod-typed json-render catalog. This makes Storybook the single source of truth for generative UI: stories define props, prop types become Zod constraints, AI can only generate components that have stories.

This is the implementation for issue #1529 (Lane C โ€ข Tier B): genui-architect imports Storybook stories as AI-safe catalog.

Workflow

Storybook stories
   โ”‚
   โ”œโ”€[ @storybook/addon-mcp ]โ”€โ–ถ list-all-documentation tool
   โ”‚                              โ”‚
   โ”‚                              โ–ผ
   โ”‚                            JSON manifest (components + argTypes)
   โ”‚
   โ””โ”€[ storybook-to-catalog.mjs ]โ”€โ”€โ–ถ catalog.ts (Zod schemas) + components.tsx (registry)
  1. Run Storybook with @storybook/addon-mcp enabled (see ork:storybook-mcp-integration).
  2. Capture the manifest:
    curl -s http://localhost:6006/mcp -X POST \
      -H 'Content-Type: application/json' \
      -d '{"method":"tools/call","params":{"name":"list-all-documentation"}}' \
      > storybook-manifest.json
  3. Generate the catalog:
    node "${CLAUDE_SKILL_DIR}/scripts/storybook-to-catalog.mjs" \
      storybook-manifest.json \
      --out src/genui/catalog.ts
  4. Review the generated catalog. Tune individual schemas if AI is generating unsafe values.

Storybook ArgType โ†’ Zod mapping

The script applies a deterministic mapping. Anything outside this list is dropped from the catalog with a warning โ€” AI safety first; you can add it back manually after review.

Storybook argZodNotes
\{ control: 'text' \}z.string().max(500)Length cap prevents prompt-injection-via-text
\{ control: 'number' \}z.number()If min/max set, applied via .min().max()
\{ control: 'boolean' \}z.boolean()โ€”
\{ control: 'select', options: [...] \}z.enum([...])Best case โ€” fully constrained
\{ control: 'radio', options: [...] \}z.enum([...])Same
\{ control: 'color' \}z.string().regex(/^#[0-9a-fA-F]\{6\}$/)Hex only
\{ control: 'date' \}z.string().datetime()ISO-8601
\{ control: 'object' \}DROPPEDToo open-ended for AI safety; add manually with explicit shape
\{ control: 'array' \}z.array(z.string()).max(20)Length cap; assumed string elements
TypeScript ReactNodechildren: 'allowed'Marks the catalog entry as a container
TypeScript () => void (callbacks)DROPPEDAI cannot generate functions; the registry wires them

Output

The script emits two files:

catalog.ts

// AUTO-GENERATED from Storybook โ€” do not edit by hand
// Source: storybook-manifest.json (sha-1: <hash>)
// Generated: 2026-04-28T05:00:00Z
import { defineCatalog } from '@json-render/core'
import { z } from 'zod'

export const catalog = defineCatalog({
  Card: {
    description: 'Card component with optional title and elevation',
    props: z.object({
      title: z.string().max(500).optional(),
      elevation: z.enum(['flat', 'low', 'high']),
    }),
    children: 'allowed',
  },
  Button: {
    description: 'Button with size and variant',
    props: z.object({
      label: z.string().max(500),
      size: z.enum(['sm', 'md', 'lg']),
      variant: z.enum(['primary', 'secondary', 'ghost']),
      disabled: z.boolean().optional(),
    }),
    children: false,
  },
  // ...
})

components.tsx

// AUTO-GENERATED from Storybook โ€” wires catalog to actual React components
// Edit imports if your story files live elsewhere.
import type { CatalogComponents } from '@json-render/react'
import type { catalog } from './catalog'
import { Card } from '../components/Card'
import { Button } from '../components/Button'

export const components: CatalogComponents<typeof catalog> = {
  Card,
  Button,
  // ...
}

The components.tsx import paths are derived from the story file paths in the manifest (e.g. src/components/Card/Card.stories.tsx โ†’ import \{ Card \} from '../components/Card/Card'). Always review the imports โ€” story file colocation conventions vary.

Validation

The script validates:

  • Every emitted Zod schema parses cleanly (round-trip check)
  • No z.any() or z.unknown() slips into the catalog (would defeat AI safety)
  • Component names are unique
  • At least one component is exported (otherwise the catalog is useless)

On any validation failure the script exits 1 and emits no files. The dropped-prop log is always written to stderr so you can see what was skipped.

Genui-architect integration

The genui-architect agent has a "Storybook import" task path (see agent file). When the user has a Storybook setup, the agent should:

  1. Probe for the Storybook MCP via ToolSearch(query="+storybook list-all-documentation")
  2. If available, capture the manifest and run this script โ€” emit the catalog as the starting point
  3. Hand-tune individual schemas where AI safety demands tighter constraints than the auto-mapping produces
  4. Verify the catalog is wired by sample-rendering a few stories via mcp__storybook-mcp__preview-stories

When Storybook MCP is not available, fall back to the existing manual catalog design workflow documented in json-render-catalog/SKILL.md.

Why this matters

Without this importer, teams using both Storybook and json-render maintain two parallel definitions: the story file (props + canonical examples) and the catalog (Zod schemas). They drift. The Storybook manifest already carries everything needed to generate the catalog โ€” emitting it once and regenerating on demand keeps Storybook as the single source of truth and eliminates the drift class entirely.

Upstream Core

<!-- SYNCED from vercel-labs/json-render (skills/core/SKILL.md) --> <!-- Hash: 70cfc936a6c0f1dfdf52d08a5f4ffb39da1cf9dcd4624750a092fedfaa4fd5d9 --> <!-- Re-sync: bash scripts/sync-vercel-skills.sh -->

@json-render/core

Core package for schema definition, catalog creation, and spec streaming.

Key Concepts

  • Schema: Defines the structure of specs and catalogs (use defineSchema)
  • Catalog: Maps component/action names to their definitions (use defineCatalog)
  • Spec: JSON output from AI that conforms to the schema
  • SpecStream: JSONL streaming format for progressive spec building

Defining a Schema

import { defineSchema } from "@json-render/core";

export const schema = defineSchema((s) => ({
  spec: s.object({
    // Define spec structure
  }),
  catalog: s.object({
    components: s.map({
      props: s.zod(),
      description: s.string(),
    }),
  }),
}), {
  promptTemplate: myPromptTemplate, // Optional custom AI prompt
});

Creating a Catalog

import { defineCatalog } from "@json-render/core";
import { schema } from "./schema";
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 component",
    },
  },
});

Generating AI Prompts

const systemPrompt = catalog.prompt(); // Uses schema's promptTemplate
const systemPrompt = catalog.prompt({ customRules: ["Rule 1", "Rule 2"] });

SpecStream Utilities

For streaming AI responses (JSONL patches):

import { createSpecStreamCompiler } from "@json-render/core";

const compiler = createSpecStreamCompiler<MySpec>();

// Process streaming chunks
const { result, newPatches } = compiler.push(chunk);

// Get final result
const finalSpec = compiler.getResult();

Dynamic Prop Expressions

Any prop value can be a dynamic expression resolved at render time:

  • \{ "$state": "/state/key" \} - reads a value from the state model (one-way read)
  • \{ "$bindState": "/path" \} - two-way binding: reads from state and enables write-back. Use on the natural value prop (value, checked, pressed, etc.) of form components.
  • \{ "$bindItem": "field" \} - two-way binding to a repeat item field. Use inside repeat scopes.
  • \{ "$cond": &lt;condition&gt;, "$then": &lt;value&gt;, "$else": &lt;value&gt; \} - evaluates a visibility condition and picks a branch
  • \{ "$template": "Hello, $\{/user/name\}!" \} - interpolates $\{/path\} references with state values
  • \{ "$computed": "fnName", "args": \{ "key": &lt;expression&gt; \} \} - calls a registered function with resolved args

$cond uses the same syntax as visibility conditions ($state, eq, neq, not, arrays for AND). $then and $else can themselves be expressions (recursive).

Components do not use a statePath prop for two-way binding. Instead, use \{ "$bindState": "/path" \} on the natural value prop (e.g. value, checked, pressed).

{
  "color": {
    "$cond": { "$state": "/activeTab", "eq": "home" },
    "$then": "#007AFF",
    "$else": "#8E8E93"
  },
  "label": { "$template": "Welcome, ${/user/name}!" },
  "fullName": {
    "$computed": "fullName",
    "args": {
      "first": { "$state": "/form/firstName" },
      "last": { "$state": "/form/lastName" }
    }
  }
}
import { resolvePropValue, resolveElementProps } from "@json-render/core";

const resolved = resolveElementProps(element.props, { stateModel: myState });

State Watchers

Elements can declare a watch field (top-level, sibling of type/props/children) to trigger actions when state values change:

{
  "type": "Select",
  "props": { "value": { "$bindState": "/form/country" }, "options": ["US", "Canada"] },
  "watch": {
    "/form/country": { "action": "loadCities", "params": { "country": { "$state": "/form/country" } } }
  },
  "children": []
}

Watchers only fire on value changes, not on initial render.

Validation

Built-in validation functions: required, email, url, numeric, minLength, maxLength, min, max, pattern, matches, equalTo, lessThan, greaterThan, requiredIf.

Cross-field validation uses $state expressions in args:

import { check } from "@json-render/core";

check.required("Field is required");
check.matches("/form/password", "Passwords must match");
check.lessThan("/form/endDate", "Must be before end date");
check.greaterThan("/form/startDate", "Must be after start date");
check.requiredIf("/form/enableNotifications", "Required when enabled");

User Prompt Builder

Build structured user prompts with optional spec refinement and state context:

import { buildUserPrompt } from "@json-render/core";

// Fresh generation
buildUserPrompt({ prompt: "create a todo app" });

// Refinement with edit modes (default: patch-only)
buildUserPrompt({ prompt: "add a toggle", currentSpec: spec, editModes: ["patch", "merge"] });

// With runtime state
buildUserPrompt({ prompt: "show data", state: { todos: [] } });

Available edit modes: "patch" (RFC 6902 JSON Patch), "merge" (RFC 7396 Merge Patch), "diff" (unified diff).

Spec Validation

Validate spec structure and auto-fix common issues:

import { validateSpec, autoFixSpec } from "@json-render/core";

const { valid, issues } = validateSpec(spec);
const fixed = autoFixSpec(spec);

Visibility Conditions

Control element visibility with state-based conditions. VisibilityContext is \{ stateModel: StateModel \}.

import { visibility } from "@json-render/core";

// Syntax
{ "$state": "/path" }                    // truthiness
{ "$state": "/path", "not": true }      // falsy
{ "$state": "/path", "eq": value }      // equality
[ cond1, cond2 ]                         // implicit AND

// Helpers
visibility.when("/path")                 // { $state: "/path" }
visibility.unless("/path")               // { $state: "/path", not: true }
visibility.eq("/path", val)              // { $state: "/path", eq: val }
visibility.and(cond1, cond2)             // { $and: [cond1, cond2] }
visibility.or(cond1, cond2)              // { $or: [cond1, cond2] }
visibility.always                        // true
visibility.never                         // false

Built-in Actions in Schema

Schemas can declare builtInActions -- actions that are always available at runtime and auto-injected into prompts:

const schema = defineSchema(builder, {
  builtInActions: [
    { name: "setState", description: "Update a value in the state model" },
  ],
});

These appear in prompts as [built-in] and don't require handlers in defineRegistry.

StateStore

The StateStore interface allows external state management libraries (Redux, Zustand, XState, etc.) to be plugged into json-render renderers. The createStateStore factory creates a simple in-memory implementation:

import { createStateStore, type StateStore } from "@json-render/core";

const store = createStateStore({ count: 0 });

store.get("/count");         // 0
store.set("/count", 1);      // updates and notifies subscribers
store.update({ "/a": 1, "/b": 2 }); // batch update

store.subscribe(() => {
  console.log(store.getSnapshot()); // { count: 1 }
});

The StateStore interface: get(path), set(path, value), update(updates), getSnapshot(), subscribe(listener).

Key Exports

ExportPurpose
defineSchemaCreate a new schema
defineCatalogCreate a catalog from schema
createStateStoreCreate a framework-agnostic in-memory StateStore
resolvePropValueResolve a single prop expression against data
resolveElementPropsResolve all prop expressions in an element
buildUserPromptBuild user prompts with refinement and state context
buildEditUserPromptBuild user prompt for editing existing specs
buildEditInstructionsGenerate prompt section for available edit modes
isNonEmptySpecCheck if spec has root and at least one element
deepMergeSpecRFC 7396 deep merge (null deletes, arrays replace, objects recurse)
diffToPatchesGenerate RFC 6902 JSON Patch operations from object diff
EditModeType: "patch" | "merge" | "diff"
validateSpecValidate spec structure
autoFixSpecAuto-fix common spec issues
createSpecStreamCompilerStream JSONL patches into spec
createJsonRenderTransformTransformStream separating text from JSONL in mixed streams
parseSpecStreamLineParse single JSONL line
applySpecStreamPatchApply patch to object
StateStoreInterface for plugging in external state management
ComputedFunctionFunction signature for $computed expressions
checkTypeScript helpers for creating validation checks
BuiltInActionType for built-in action definitions (name + description)
ActionBindingAction binding type (includes preventDefault field)

Upstream Email

<!-- SYNCED from vercel-labs/json-render (skills/react-email/SKILL.md) --> <!-- Hash: 8b376b37da07dd68944e72e0e322a272571f39271e5b08b6efd437db201470c9 --> <!-- Re-sync: bash scripts/sync-vercel-skills.sh -->

@json-render/react-email

React Email renderer that converts JSON specs into HTML or plain-text email output.

Quick Start

import { renderToHtml } from "@json-render/react-email";
import { schema, standardComponentDefinitions } from "@json-render/react-email";
import { defineCatalog } from "@json-render/core";

const catalog = defineCatalog(schema, {
  components: standardComponentDefinitions,
});

const spec = {
  root: "html-1",
  elements: {
    "html-1": { type: "Html", props: { lang: "en", dir: "ltr" }, children: ["head-1", "body-1"] },
    "head-1": { type: "Head", props: {}, children: [] },
    "body-1": {
      type: "Body",
      props: { style: { backgroundColor: "#f6f9fc" } },
      children: ["container-1"],
    },
    "container-1": {
      type: "Container",
      props: { style: { maxWidth: "600px", margin: "0 auto", padding: "20px" } },
      children: ["heading-1", "text-1"],
    },
    "heading-1": { type: "Heading", props: { text: "Welcome" }, children: [] },
    "text-1": { type: "Text", props: { text: "Thanks for signing up." }, children: [] },
  },
};

const html = await renderToHtml(spec);

Spec Structure (Element Tree)

Same flat element tree as @json-render/react: root key plus elements map. Root must be Html; children of Html should be Head and Body. Use Container (e.g. max-width 600px) inside Body for client-safe layout.

Creating a Catalog and Registry

import { defineCatalog } from "@json-render/core";
import { schema, defineRegistry, renderToHtml } from "@json-render/react-email";
import { standardComponentDefinitions } from "@json-render/react-email/catalog";
import { Container, Heading, Text } from "@react-email/components";
import { z } from "zod";

const catalog = defineCatalog(schema, {
  components: {
    ...standardComponentDefinitions,
    Alert: {
      props: z.object({
        message: z.string(),
        variant: z.enum(["info", "success", "warning"]).nullable(),
      }),
      slots: [],
      description: "A highlighted message block",
    },
  },
  actions: {},
});

const { registry } = defineRegistry(catalog, {
  components: {
    Alert: ({ props }) => (
      <Container style={{ padding: 16, backgroundColor: "#eff6ff", borderRadius: 8 }}>
        <Text style={{ margin: 0 }}>{props.message}</Text>
      </Container>
    ),
  },
});

const html = await renderToHtml(spec, { registry });

Server-Side Render APIs

FunctionPurpose
renderToHtml(spec, options?)Render spec to HTML email string
renderToPlainText(spec, options?)Render spec to plain-text email string

RenderOptions: registry, includeStandard (default true), state (for $state / $cond).

Visibility and State

Supports visible conditions, $state, $cond, repeat (repeat.statePath), and the same expression syntax as @json-render/react. Use state in RenderOptions when rendering server-side so expressions resolve.

Server-Safe Import

Import schema and catalog without React or @react-email/components:

import { schema, standardComponentDefinitions } from "@json-render/react-email/server";

Key Exports

ExportPurpose
defineRegistryCreate type-safe component registry from catalog
RendererRender spec in browser (e.g. preview); use with JSONUIProvider for state/actions
createRendererStandalone renderer component with state/actions/validation
renderToHtmlServer: spec to HTML string
renderToPlainTextServer: spec to plain-text string
schemaEmail element schema
standardComponentsPre-built component implementations
standardComponentDefinitionsCatalog definitions (Zod props)

Sub-path Exports

PathPurpose
@json-render/react-emailFull package
@json-render/react-email/serverSchema and catalog only (no React)
@json-render/react-email/catalogStandard component definitions and types
@json-render/react-email/renderRender functions only

Standard Components

All components accept a style prop (object) for inline styles. Use inline styles for email client compatibility; avoid external CSS.

Document structure

ComponentDescription
HtmlRoot wrapper (lang, dir). Children: Head, Body.
HeadEmail head section.
BodyBody wrapper; use style for background.

Layout

ComponentDescription
ContainerConstrain width (e.g. max-width 600px).
SectionGroup content; table-based for compatibility.
RowHorizontal row.
ColumnColumn in a Row; set width via style.

Content

ComponentDescription
HeadingHeading text (as: h1โ€“h6).
TextBody text.
LinkHyperlink (text, href).
ButtonCTA link styled as button (text, href).
ImageImage from URL (src, alt, width, height).
HrHorizontal rule.

Utility

ComponentDescription
PreviewInbox preview text (inside Html).
MarkdownMarkdown content as email-safe HTML.

Email Best Practices

  • Keep width constrained (e.g. Container max-width 600px).
  • Use inline styles or React Email's style props; many clients strip &lt;style&gt; blocks.
  • Prefer table-based layout (Section, Row, Column) for broad client support.
  • Use absolute URLs for images; many clients block relative or cid: references in some contexts.
  • Test in multiple clients (Gmail, Outlook, Apple Mail); use a preview tool or Litmus-like service when possible.

Upstream Image

<!-- SYNCED from vercel-labs/json-render (skills/image/SKILL.md) --> <!-- Hash: fc6469e1592a86d4d92058b81704023394ab8cbdd9f422d541ad3a752f2a0e42 --> <!-- Re-sync: bash scripts/sync-vercel-skills.sh -->

@json-render/image

Image renderer that converts JSON specs into SVG and PNG images using Satori.

Quick Start

import { renderToPng } from "@json-render/image/render";
import type { Spec } from "@json-render/core";

const spec: Spec = {
  root: "frame",
  elements: {
    frame: {
      type: "Frame",
      props: { width: 1200, height: 630, backgroundColor: "#1a1a2e" },
      children: ["heading"],
    },
    heading: {
      type: "Heading",
      props: { text: "Hello World", level: "h1", color: "#ffffff" },
      children: [],
    },
  },
};

const png = await renderToPng(spec, {
  fonts: [{ name: "Inter", data: fontData, weight: 400, style: "normal" }],
});

Using Standard Components

import { defineCatalog } from "@json-render/core";
import { schema, standardComponentDefinitions } from "@json-render/image";

export const imageCatalog = defineCatalog(schema, {
  components: standardComponentDefinitions,
});

Adding Custom Components

import { z } from "zod";

const catalog = defineCatalog(schema, {
  components: {
    ...standardComponentDefinitions,
    Badge: {
      props: z.object({ label: z.string(), color: z.string().nullable() }),
      slots: [],
      description: "A colored badge label",
    },
  },
});

Standard Components

ComponentCategoryDescription
FrameRootRoot container. Defines width, height, background. Must be root.
BoxLayoutContainer with padding, margin, border, absolute positioning
RowLayoutHorizontal flex layout
ColumnLayoutVertical flex layout
HeadingContenth1-h4 heading text
TextContentBody text with full styling
ImageContentImage from URL
DividerDecorativeHorizontal line separator
SpacerDecorativeEmpty vertical space

Key Exports

ExportPurpose
renderToSvgRender spec to SVG string
renderToPngRender spec to PNG buffer (requires @resvg/resvg-js)
schemaImage element schema
standardComponentsPre-built component registry
standardComponentDefinitionsCatalog definitions for AI prompts

Sub-path Exports

ExportDescription
@json-render/imageFull package: schema, components, render functions
@json-render/image/serverSchema and catalog definitions only (no React/Satori)
@json-render/image/catalogStandard component definitions and types
@json-render/image/renderRender functions only

Upstream Ink

<!-- SYNCED from vercel-labs/json-render (skills/ink/SKILL.md) --> <!-- Hash: 10721bbc7ee13fc084cc08ec3a0a4eda86b5b7fbc97c2ea82cfc8994bc8ea7f0 --> <!-- Re-sync: bash scripts/sync-vercel-skills.sh -->

@json-render/ink

Ink terminal renderer that converts JSON specs into interactive terminal component trees with standard components, data binding, visibility, actions, and dynamic props.

Quick Start

import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/ink/schema";
import {
  standardComponentDefinitions,
  standardActionDefinitions,
} from "@json-render/ink/catalog";
import { defineRegistry, Renderer, type Components } from "@json-render/ink";
import { z } from "zod";

// Create catalog with standard + custom components
const catalog = defineCatalog(schema, {
  components: {
    ...standardComponentDefinitions,
    CustomWidget: {
      props: z.object({ title: z.string() }),
      slots: [],
      description: "Custom widget",
    },
  },
  actions: standardActionDefinitions,
});

// Register only custom components (standard ones are built-in)
const { registry } = defineRegistry(catalog, {
  components: {
    CustomWidget: ({ props }) => <Text>{props.title}</Text>,
  } as Components<typeof catalog>,
});

// Render
function App({ spec }) {
  return (
    <JSONUIProvider initialState={{}}>
      <Renderer spec={spec} registry={registry} />
    </JSONUIProvider>
  );
}

Spec Structure (Flat Element Map)

The Ink schema uses a flat element map with a root key:

{
  "root": "main",
  "elements": {
    "main": {
      "type": "Box",
      "props": { "flexDirection": "column", "padding": 1 },
      "children": ["heading", "content"]
    },
    "heading": {
      "type": "Heading",
      "props": { "text": "Dashboard", "level": "h1" },
      "children": []
    },
    "content": {
      "type": "Text",
      "props": { "text": "Hello from the terminal!" },
      "children": []
    }
  }
}

Standard Components

Layout

  • Box - Flexbox layout container (like a terminal <div>). Use for grouping, spacing, borders, alignment. Default flexDirection is row.
  • Text - Text output with optional styling (color, bold, italic, etc.)
  • Newline - Inserts blank lines. Must be inside a Box with flexDirection column.
  • Spacer - Flexible empty space that expands along the main axis.

Content

  • Heading - Section heading (h1: bold+underlined, h2: bold, h3: bold+dimmed, h4: dimmed)
  • Divider - Horizontal separator with optional centered title
  • Badge - Colored inline label (variants: default, info, success, warning, error)
  • Spinner - Animated loading spinner with optional label
  • ProgressBar - Horizontal progress bar (0-1)
  • Sparkline - Inline chart using Unicode block characters
  • BarChart - Horizontal bar chart with labels and values
  • Table - Tabular data with headers and rows
  • List - Bulleted or numbered list
  • ListItem - Structured list row with title, subtitle, leading/trailing text
  • Card - Bordered container with optional title
  • KeyValue - Key-value pair display
  • Link - Clickable URL with optional label
  • StatusLine - Status message with colored icon (info, success, warning, error)
  • Markdown - Renders markdown text with terminal styling

Interactive

  • TextInput - Text input field (events: submit, change)
  • Select - Selection menu with arrow key navigation (events: change)
  • MultiSelect - Multi-selection with space to toggle (events: change, submit)
  • ConfirmInput - Yes/No confirmation prompt (events: confirm, deny)
  • Tabs - Tab bar navigation with left/right arrow keys (events: change)

Visibility Conditions

Use visible on elements to show/hide based on state. Syntax: \{ "$state": "/path" \}, \{ "$state": "/path", "eq": value \}, \{ "$state": "/path", "not": true \}, \{ "$and": [cond1, cond2] \} for AND, \{ "$or": [cond1, cond2] \} for OR.

Dynamic Prop Expressions

Any prop value can be a data-driven expression resolved at render time:

  • \{ "$state": "/state/key" \} - reads from state model (one-way read)
  • \{ "$bindState": "/path" \} - two-way binding: use on the natural value prop of form components
  • \{ "$bindItem": "field" \} - two-way binding to a repeat item field
  • \{ "$cond": &lt;condition&gt;, "$then": &lt;value&gt;, "$else": &lt;value&gt; \} - conditional value
  • \{ "$template": "Hello, $\{/name\}!" \} - interpolates state values into strings

Components do not use a statePath prop for two-way binding. Use \{ "$bindState": "/path" \} on the natural value prop instead.

Event System

Components use emit to fire named events. The element's on field maps events to action bindings:

CustomButton: ({ props, emit }) => (
  <Box>
    <Text>{props.label}</Text>
    {/* emit("press") triggers the action bound in the spec's on.press */}
  </Box>
),
{
  "type": "CustomButton",
  "props": { "label": "Submit" },
  "on": { "press": { "action": "submit" } },
  "children": []
}

Built-in Actions

setState, pushState, and removeState are built-in and handled automatically:

{ "action": "setState", "params": { "statePath": "/activeTab", "value": "home" } }
{ "action": "pushState", "params": { "statePath": "/items", "value": { "text": "New" } } }
{ "action": "removeState", "params": { "statePath": "/items", "index": 0 } }

Repeat (Dynamic Lists)

Use the repeat field on a container element to render items from a state array:

{
  "type": "Box",
  "props": { "flexDirection": "column" },
  "repeat": { "statePath": "/items", "key": "id" },
  "children": ["item-row"]
}

Inside repeated children, use \{ "$item": "field" \} to read from the current item and \{ "$index": true \} for the current index.

Streaming

Use useUIStream to progressively render specs from JSONL patch streams:

import { useUIStream } from "@json-render/ink";

const { spec, send, isStreaming } = useUIStream({ api: "/api/generate" });

Server-Side Prompt Generation

Use the ./server export to generate AI system prompts from your catalog:

import { catalog } from "./catalog";

const systemPrompt = catalog.prompt({ system: "You are a terminal assistant." });

Providers

ProviderPurpose
StateProviderShare state across components (JSON Pointer paths). Accepts optional store prop for controlled mode.
ActionProviderHandle actions dispatched via the event system
VisibilityProviderEnable conditional rendering based on state
ValidationProviderForm field validation
FocusProviderManage focus across interactive components
JSONUIProviderCombined provider for all contexts

External Store (Controlled Mode)

Pass a StateStore to StateProvider (or JSONUIProvider) to use external state management:

import { createStateStore, type StateStore } from "@json-render/ink";

const store = createStateStore({ count: 0 });

<StateProvider store={store}>{children}</StateProvider>

store.set("/count", 1); // React re-renders automatically

When store is provided, initialState and onStateChange are ignored.

createRenderer (Higher-Level API)

import { createRenderer } from "@json-render/ink";
import { standardComponents } from "@json-render/ink";
import { catalog } from "./catalog";

const InkRenderer = createRenderer(catalog, {
  ...standardComponents,
  // custom component overrides here
});

// InkRenderer includes all providers (state, visibility, actions, focus)
render(
  <InkRenderer spec={spec} state={{ activeTab: "overview" }} />
);

Key Exports

ExportPurpose
defineRegistryCreate a type-safe component registry from a catalog
RendererRender a spec using a registry
createRendererHigher-level: creates a component with built-in providers
JSONUIProviderCombined provider for all contexts
schemaInk flat element map schema (includes built-in state actions)
standardComponentDefinitionsCatalog definitions for all standard components
standardActionDefinitionsCatalog definitions for standard actions
standardComponentsPre-built component implementations
useStateStoreAccess state context
useStateValueGet single value from state
useBoundPropTwo-way binding for $bindState/$bindItem expressions
useActionsAccess actions context
useActionGet a single action dispatch function
useOptionalValidationNon-throwing variant of useValidation
useUIStreamStream specs from an API endpoint
createStateStoreCreate a framework-agnostic in-memory StateStore
StateStoreInterface for plugging in external state management
ComponentsTyped component map (catalog-aware)
ActionsTyped action map (catalog-aware)
ComponentContextTyped component context (catalog-aware)
flatToTreeConvert flat element map to tree structure

Terminal UI Design Guidelines

  • Use Box for layout (flexDirection, padding, gap). Default flexDirection is row.
  • Terminal width is ~80-120 columns. Prefer vertical layouts (flexDirection: column) for main structure.
  • Use borderStyle on Box for visual grouping (single, double, round, bold).
  • Use named terminal colors: red, green, yellow, blue, magenta, cyan, white, gray.
  • Use Heading for section titles, Divider to separate sections, Badge for status, KeyValue for labeled data, Card for bordered groups.
  • Use Tabs for multi-view UIs with visible conditions on child content.
  • Use Sparkline for inline trends and BarChart for comparing values.

Upstream Jotai

<!-- SYNCED from vercel-labs/json-render (skills/jotai/SKILL.md) --> <!-- Hash: 62d41ffc480e1d4b3517b599d4a18cba8a8894009d7fe42933f829b6067789a7 --> <!-- Re-sync: bash scripts/sync-vercel-skills.sh -->

@json-render/jotai

Jotai adapter for json-render's StateStore interface. Wire a Jotai atom as the state backend for json-render.

Installation

npm install @json-render/jotai @json-render/core @json-render/react jotai

Usage

import { atom } from "jotai";
import { jotaiStateStore } from "@json-render/jotai";
import { StateProvider } from "@json-render/react";

// 1. Create an atom that holds the json-render state
const uiAtom = atom<Record<string, unknown>>({ count: 0 });

// 2. Create the json-render StateStore adapter
const store = jotaiStateStore({ atom: uiAtom });

// 3. Use it
<StateProvider store={store}>
  {/* json-render reads/writes go through Jotai */}
</StateProvider>

With a Shared Jotai Store

When your app already uses a Jotai &lt;Provider&gt; with a custom store, pass it so both json-render and your components share the same state:

import { atom, createStore } from "jotai";
import { Provider as JotaiProvider } from "jotai/react";
import { jotaiStateStore } from "@json-render/jotai";
import { StateProvider } from "@json-render/react";

const jStore = createStore();
const uiAtom = atom<Record<string, unknown>>({ count: 0 });

const store = jotaiStateStore({ atom: uiAtom, store: jStore });

<JotaiProvider store={jStore}>
  <StateProvider store={store}>
    {/* Both json-render and useAtom() see the same state */}
  </StateProvider>
</JotaiProvider>

API

jotaiStateStore(options)

Creates a StateStore backed by a Jotai atom.

OptionTypeRequiredDescription
atomWritableAtom&lt;StateModel, [StateModel], void&gt;YesA writable atom holding the state model
storeJotai StoreNoThe Jotai store instance. Defaults to a new store. Pass your own to share state with &lt;Provider&gt;.

Upstream Mcp

<!-- SYNCED from vercel-labs/json-render (skills/mcp/SKILL.md) --> <!-- Hash: fdc45b80ea851e518ba1ce37cbc6bdfff8627512caca55265f1f85b8639c662d --> <!-- Re-sync: bash scripts/sync-vercel-skills.sh -->

@json-render/mcp

MCP Apps integration that serves json-render UIs as interactive MCP Apps inside Claude, ChatGPT, Cursor, VS Code, and other MCP-capable clients.

Quick Start

Server (Node.js)

import { createMcpApp } from "@json-render/mcp";
import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/react/schema";
import { shadcnComponentDefinitions } from "@json-render/shadcn/catalog";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import fs from "node:fs";

const catalog = defineCatalog(schema, {
  components: { ...shadcnComponentDefinitions },
  actions: {},
});

const server = createMcpApp({
  name: "My App",
  version: "1.0.0",
  catalog,
  html: fs.readFileSync("dist/index.html", "utf-8"),
});

await server.connect(new StdioServerTransport());

Client (React, inside iframe)

import { useJsonRenderApp } from "@json-render/mcp/app";
import { JSONUIProvider, Renderer } from "@json-render/react";

function McpAppView({ registry }) {
  const { spec, loading, error } = useJsonRenderApp();
  if (error) return <div>Error: {error.message}</div>;
  if (!spec) return <div>Waiting...</div>;
  return (
    <JSONUIProvider registry={registry} initialState={spec.state ?? {}}>
      <Renderer spec={spec} registry={registry} loading={loading} />
    </JSONUIProvider>
  );
}

Architecture

  1. createMcpApp() creates an McpServer that registers a render-ui tool and a ui:// HTML resource
  2. The tool description includes the catalog prompt so the LLM knows how to generate valid specs
  3. The HTML resource is a Vite-bundled single-file React app with json-render renderers
  4. Inside the iframe, useJsonRenderApp() connects to the host via postMessage and renders specs

Server API

  • createMcpApp(options) - main entry, creates a full MCP server
  • registerJsonRenderTool(server, options) - register a json-render tool on an existing server
  • registerJsonRenderResource(server, options) - register the UI resource

Client API (@json-render/mcp/app)

  • useJsonRenderApp(options?) - React hook, returns \{ spec, loading, connected, error, callServerTool \}
  • buildAppHtml(options) - generate HTML from bundled JS/CSS

Building the iframe HTML

Bundle the React app into a single self-contained HTML file using Vite + vite-plugin-singlefile:

// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { viteSingleFile } from "vite-plugin-singlefile";

export default defineConfig({
  plugins: [react(), viteSingleFile()],
  build: { outDir: "dist" },
});

Client Configuration

Cursor (.cursor/mcp.json)

{
  "mcpServers": {
    "my-app": {
      "command": "npx",
      "args": ["tsx", "server.ts", "--stdio"]
    }
  }
}

Claude Desktop

{
  "mcpServers": {
    "my-app": {
      "command": "npx",
      "args": ["tsx", "/path/to/server.ts", "--stdio"]
    }
  }
}

Dependencies

# Server
npm install @json-render/mcp @json-render/core @modelcontextprotocol/sdk

# Client (iframe)
npm install @json-render/react @json-render/shadcn react react-dom

# Build tools
npm install -D vite @vitejs/plugin-react vite-plugin-singlefile

Upstream Next

<!-- SYNCED from vercel-labs/json-render (skills/next/SKILL.md) --> <!-- Hash: 2a4b7b3694ec204bf08a09bb7074300a963183ed015920fb807a75dbc14cd3fa --> <!-- Re-sync: bash scripts/sync-vercel-skills.sh -->

@json-render/next

Next.js renderer that converts JSON specs into full Next.js applications with routes, pages, layouts, metadata, and SSR support.

Quick Start

npm install @json-render/core @json-render/react @json-render/next

1. Define Your Spec

// lib/spec.ts
import type { NextAppSpec } from "@json-render/next";

export const spec: NextAppSpec = {
  metadata: {
    title: { default: "My App", template: "%s | My App" },
    description: "A json-render Next.js application",
  },
  layouts: {
    main: {
      root: "shell",
      elements: {
        shell: { type: "Container", props: {}, children: ["nav", "slot"] },
        nav: { type: "NavBar", props: { links: [
          { href: "/", label: "Home" },
          { href: "/about", label: "About" },
        ]}, children: [] },
        slot: { type: "Slot", props: {}, children: [] },
      },
    },
  },
  routes: {
    "/": {
      layout: "main",
      metadata: { title: "Home" },
      page: {
        root: "hero",
        elements: {
          hero: { type: "Card", props: { title: "Welcome" }, children: [] },
        },
      },
    },
    "/about": {
      layout: "main",
      metadata: { title: "About" },
      page: {
        root: "content",
        elements: {
          content: { type: "Card", props: { title: "About Us" }, children: [] },
        },
      },
    },
  },
};

2. Create the App

// lib/app.ts
import { createNextApp } from "@json-render/next/server";
import { spec } from "./spec";

export const { Page, generateMetadata, generateStaticParams } = createNextApp({
  spec,
  loaders: {
    // Server-side data loaders (optional)
    loadPost: async ({ slug }) => {
      const post = await getPost(slug as string);
      return { post };
    },
  },
});

3. Wire Up Route Files

// app/[[...slug]]/page.tsx
export { Page as default, generateMetadata, generateStaticParams } from "@/lib/app";
// app/[[...slug]]/layout.tsx
import { NextAppProvider } from "@json-render/next";
import { registry, handlers } from "@/lib/registry";

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <NextAppProvider registry={registry} handlers={handlers}>
          {children}
        </NextAppProvider>
      </body>
    </html>
  );
}

Key Concepts

NextAppSpec

The top-level spec defines an entire Next.js application:

  • metadata: Root-level SEO metadata (title template, description, OpenGraph)
  • layouts: Reusable layout element trees (each must include a Slot component)
  • routes: Route definitions keyed by URL pattern
  • state: Global initial state shared across all routes

Route Patterns

Routes use Next.js URL conventions:

  • "/" -- home page
  • "/about" -- static route
  • "/blog/[slug]" -- dynamic segment
  • "/docs/[...path]" -- catch-all segment
  • "/settings/[[...path]]" -- optional catch-all segment

Layouts

Layouts wrap page content. Every layout MUST include a Slot component where page content will be rendered. Layouts are defined once in spec.layouts and referenced by routes via the layout field.

Built-in Components

  • Slot: Placeholder in layouts where page content is rendered
  • Link: Client-side navigation link (wraps next/link)

Built-in Actions

  • setState: Update state value. Params: \{ statePath, value \}
  • pushState: Append to array. Params: \{ statePath, value, clearStatePath? \}
  • removeState: Remove from array by index. Params: \{ statePath, index \}
  • navigate: Client-side navigation. Params: \{ href \}

Data Loaders

Server-side async functions that run in the Server Component before rendering. Results are merged into the page's initial state.

createNextApp({
  spec,
  loaders: {
    loadPost: async ({ slug }) => {
      const post = await db.post.findUnique({ where: { slug } });
      return { post };
    },
  },
});

SSR

Pages are server-rendered automatically. The createNextApp Page component is an async Server Component that:

  1. Matches the route from the spec
  2. Runs server-side data loaders
  3. Generates metadata
  4. Passes the resolved spec to the client renderer for hydration

Entry Points

  • @json-render/next -- Client components (NextAppProvider, PageRenderer, Link)
  • @json-render/next/server -- Server utilities (createNextApp, matchRoute, schema)

API Reference

Server Exports (@json-render/next/server)

  • createNextApp(options) -- Create Page, generateMetadata, generateStaticParams
  • schema -- Custom schema for Next.js apps (for AI catalog generation)
  • matchRoute(spec, pathname) -- Match a URL to a route spec
  • resolveMetadata(spec, route) -- Resolve metadata for a route
  • slugToPath(slug) -- Convert catch-all slug array to pathname
  • collectStaticParams(spec) -- Collect static params for all routes

Client Exports (@json-render/next)

  • NextAppProvider -- Context provider for registry and handlers
  • PageRenderer -- Renders a page spec with optional layout
  • NextErrorBoundary -- Error boundary component
  • NextLoading -- Loading state component
  • NextNotFound -- Not-found component
  • Link -- Built-in navigation component (wraps next/link)

Upstream Pdf

<!-- SYNCED from vercel-labs/json-render (skills/react-pdf/SKILL.md) --> <!-- Hash: 42d7d378733e0c1e6ff10ba788dc77c32b36b2f28841f1d6aa671e44ed9ad0be --> <!-- Re-sync: bash scripts/sync-vercel-skills.sh -->

@json-render/react-pdf

React PDF renderer that generates PDF documents from JSON specs using @react-pdf/renderer.

Installation

npm install @json-render/core @json-render/react-pdf

Quick Start

import { renderToBuffer } from "@json-render/react-pdf";
import type { Spec } from "@json-render/core";

const spec: Spec = {
  root: "doc",
  elements: {
    doc: { type: "Document", props: { title: "Invoice" }, children: ["page"] },
    page: {
      type: "Page",
      props: { size: "A4" },
      children: ["heading", "table"],
    },
    heading: {
      type: "Heading",
      props: { text: "Invoice #1234", level: "h1" },
      children: [],
    },
    table: {
      type: "Table",
      props: {
        columns: [
          { header: "Item", width: "60%" },
          { header: "Price", width: "40%", align: "right" },
        ],
        rows: [
          ["Widget A", "$10.00"],
          ["Widget B", "$25.00"],
        ],
      },
      children: [],
    },
  },
};

const buffer = await renderToBuffer(spec);

Render APIs

import { renderToBuffer, renderToStream, renderToFile } from "@json-render/react-pdf";

// In-memory buffer
const buffer = await renderToBuffer(spec);

// Readable stream (pipe to HTTP response)
const stream = await renderToStream(spec);
stream.pipe(res);

// Direct to file
await renderToFile(spec, "./output.pdf");

All render functions accept an optional second argument: \{ registry?, state?, handlers? \}.

Standard Components

ComponentDescription
DocumentTop-level PDF wrapper (must be root)
PagePage with size (A4, LETTER), orientation, margins
ViewGeneric container (padding, margin, background, border)
Row, ColumnFlex layout with gap, align, justify
Headingh1-h4 heading text
TextBody text (fontSize, color, weight, alignment)
ImageImage from URL or base64
LinkHyperlink with text and href
TableData table with typed columns and rows
ListOrdered or unordered list
DividerHorizontal line separator
SpacerEmpty vertical space
PageNumberCurrent page number and total pages

Custom Catalog

import { defineCatalog } from "@json-render/core";
import { schema, defineRegistry, renderToBuffer } from "@json-render/react-pdf";
import { standardComponentDefinitions } from "@json-render/react-pdf/catalog";
import { z } from "zod";

const catalog = defineCatalog(schema, {
  components: {
    ...standardComponentDefinitions,
    Badge: {
      props: z.object({ label: z.string(), color: z.string().nullable() }),
      slots: [],
      description: "A colored badge label",
    },
  },
  actions: {},
});

const { registry } = defineRegistry(catalog, {
  components: {
    Badge: ({ props }) => (
      <View style={{ backgroundColor: props.color ?? "#e5e7eb", padding: 4 }}>
        <Text>{props.label}</Text>
      </View>
    ),
  },
});

const buffer = await renderToBuffer(spec, { registry });

External Store (Controlled Mode)

Pass a StateStore for full control over state:

import { createStateStore } from "@json-render/react-pdf";

const store = createStateStore({ invoice: { total: 100 } });
store.set("/invoice/total", 200);

Server-Safe Import

Import schema and catalog without pulling in React:

import { schema, standardComponentDefinitions } from "@json-render/react-pdf/server";

Upstream R3f

<!-- SYNCED from vercel-labs/json-render (skills/react-three-fiber/SKILL.md) --> <!-- Hash: 1d7c0ef28bcd529f6b97fc0e63c5410564c7e64d9f0c4f8807c27077d452e33f --> <!-- Re-sync: bash scripts/sync-vercel-skills.sh -->

@json-render/react-three-fiber

React Three Fiber renderer for json-render. 19 built-in 3D components.

Two Entry Points

Entry PointExportsUse For
@json-render/react-three-fiber/catalogthreeComponentDefinitionsCatalog schemas (no R3F dependency, safe for server)
@json-render/react-three-fiberthreeComponents, ThreeRenderer, ThreeCanvas, schemasR3F implementations and renderer

Usage Pattern

Pick the 3D components you need from the standard definitions:

import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/react/schema";
import { threeComponentDefinitions } from "@json-render/react-three-fiber/catalog";
import { defineRegistry } from "@json-render/react";
import { threeComponents, ThreeCanvas } from "@json-render/react-three-fiber";

// Catalog: pick definitions
const catalog = defineCatalog(schema, {
  components: {
    Box: threeComponentDefinitions.Box,
    Sphere: threeComponentDefinitions.Sphere,
    AmbientLight: threeComponentDefinitions.AmbientLight,
    DirectionalLight: threeComponentDefinitions.DirectionalLight,
    OrbitControls: threeComponentDefinitions.OrbitControls,
  },
  actions: {},
});

// Registry: pick matching implementations
const { registry } = defineRegistry(catalog, {
  components: {
    Box: threeComponents.Box,
    Sphere: threeComponents.Sphere,
    AmbientLight: threeComponents.AmbientLight,
    DirectionalLight: threeComponents.DirectionalLight,
    OrbitControls: threeComponents.OrbitControls,
  },
});

Rendering

ThreeCanvas (convenience wrapper)

<ThreeCanvas
  spec={spec}
  registry={registry}
  shadows
  camera={{ position: [5, 5, 5], fov: 50 }}
  style={{ width: "100%", height: "100vh" }}
/>

Manual Canvas setup

import { Canvas } from "@react-three/fiber";
import { ThreeRenderer } from "@json-render/react-three-fiber";

<Canvas shadows>
  <ThreeRenderer spec={spec} registry={registry}>
    {/* Additional R3F elements */}
  </ThreeRenderer>
</Canvas>

Available Components (19)

Primitives (7)

  • Box -- width, height, depth, material
  • Sphere -- radius, widthSegments, heightSegments, material
  • Cylinder -- radiusTop, radiusBottom, height, material
  • Cone -- radius, height, material
  • Torus -- radius, tube, material
  • Plane -- width, height, material
  • Capsule -- radius, length, material

All primitives share: position, rotation, scale, castShadow, receiveShadow, material.

Lights (4)

  • AmbientLight -- color, intensity
  • DirectionalLight -- position, color, intensity, castShadow
  • PointLight -- position, color, intensity, distance, decay
  • SpotLight -- position, color, intensity, angle, penumbra

Other (8)

  • Group -- container with position/rotation/scale, supports children
  • Model -- GLTF/GLB loader via url prop
  • Environment -- HDRI environment map (preset, background, blur, intensity)
  • Fog -- linear fog (color, near, far)
  • GridHelper -- reference grid (size, divisions, color)
  • Text3D -- SDF text (text, fontSize, color, anchorX, anchorY)
  • PerspectiveCamera -- camera (position, fov, near, far, makeDefault)
  • OrbitControls -- orbit controls (enableDamping, enableZoom, autoRotate)

Shared Schemas

Reusable Zod schemas for custom 3D catalog definitions:

import { vector3Schema, materialSchema, transformProps, shadowProps } from "@json-render/react-three-fiber";
import { z } from "zod";

// Custom 3D component
const myComponentDef = {
  props: z.object({
    ...transformProps,
    ...shadowProps,
    material: materialSchema.nullable(),
    myCustomProp: z.string(),
  }),
  description: "My custom 3D component",
};

Material Schema

materialSchema = z.object({
  color: z.string().nullable(),         // default "#ffffff"
  metalness: z.number().nullable(),     // default 0
  roughness: z.number().nullable(),     // default 1
  emissive: z.string().nullable(),      // default "#000000"
  emissiveIntensity: z.number().nullable(), // default 1
  opacity: z.number().nullable(),       // default 1
  transparent: z.boolean().nullable(),  // default false
  wireframe: z.boolean().nullable(),    // default false
});

Spec Format

3D specs use the standard json-render flat element format:

{
  "root": "scene",
  "elements": {
    "scene": {
      "type": "Group",
      "props": { "position": [0, 0, 0] },
      "children": ["light", "box"]
    },
    "light": {
      "type": "AmbientLight",
      "props": { "intensity": 0.5 },
      "children": []
    },
    "box": {
      "type": "Box",
      "props": {
        "position": [0, 0.5, 0],
        "material": { "color": "#4488ff", "metalness": 0.3, "roughness": 0.7 }
      },
      "children": []
    }
  }
}

Dependencies

Peer dependencies required:

  • @react-three/fiber >= 8.0.0
  • @react-three/drei >= 9.0.0
  • three >= 0.160.0
  • react ^19.0.0
  • zod ^4.0.0

Upstream React Native

<!-- SYNCED from vercel-labs/json-render (skills/react-native/SKILL.md) --> <!-- Hash: 6fdf47f243ff033601e515fd7c138ccf3ce08d203c8e73f61e0502b0acd117b6 --> <!-- Re-sync: bash scripts/sync-vercel-skills.sh -->

@json-render/react-native

React Native renderer that converts JSON specs into native mobile component trees with standard components, data binding, visibility, actions, and dynamic props.

Quick Start

import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/react-native/schema";
import {
  standardComponentDefinitions,
  standardActionDefinitions,
} from "@json-render/react-native/catalog";
import { defineRegistry, Renderer, type Components } from "@json-render/react-native";
import { z } from "zod";

// Create catalog with standard + custom components
const catalog = defineCatalog(schema, {
  components: {
    ...standardComponentDefinitions,
    Icon: {
      props: z.object({ name: z.string(), size: z.number().nullable(), color: z.string().nullable() }),
      slots: [],
      description: "Icon display",
    },
  },
  actions: standardActionDefinitions,
});

// Register only custom components (standard ones are built-in)
const { registry } = defineRegistry(catalog, {
  components: {
    Icon: ({ props }) => <Ionicons name={props.name} size={props.size ?? 24} />,
  } as Components<typeof catalog>,
});

// Render
function App({ spec }) {
  return (
    <StateProvider initialState={{}}>
      <VisibilityProvider>
        <ActionProvider handlers={{}}>
          <Renderer spec={spec} registry={registry} />
        </ActionProvider>
      </VisibilityProvider>
    </StateProvider>
  );
}

Standard Components

Layout

  • Container - wrapper with padding, background, border radius
  • Row - horizontal flex layout with gap, alignment
  • Column - vertical flex layout with gap, alignment
  • ScrollContainer - scrollable area (vertical or horizontal)
  • SafeArea - safe area insets for notch/home indicator
  • Pressable - touchable wrapper that triggers actions on press
  • Spacer - fixed or flexible spacing
  • Divider - thin line separator

Content

  • Heading - heading text (levels 1-6)
  • Paragraph - body text
  • Label - small label text
  • Image - image display with sizing modes
  • Avatar - circular avatar image
  • Badge - small status badge
  • Chip - tag/chip for categories

Input

  • Button - pressable button with variants
  • TextInput - text input field
  • Switch - toggle switch
  • Checkbox - checkbox with label
  • Slider - range slider
  • SearchBar - search input

Feedback

  • Spinner - loading indicator
  • ProgressBar - progress indicator

Composite

  • Card - card container with optional header
  • ListItem - list row with title, subtitle, accessory
  • Modal - bottom sheet modal

Visibility Conditions

Use visible on elements. Syntax: \{ "$state": "/path" \}, \{ "$state": "/path", "eq": value \}, \{ "$state": "/path", "not": true \}, [ cond1, cond2 ] for AND.

Pressable + setState Pattern

Use Pressable with the built-in setState action for interactive UIs like tab bars:

{
  "type": "Pressable",
  "props": {
    "action": "setState",
    "actionParams": { "statePath": "/activeTab", "value": "home" }
  },
  "children": ["home-icon", "home-label"]
}

Dynamic Prop Expressions

Any prop value can be a data-driven expression resolved at render time:

  • \{ "$state": "/state/key" \} - reads from state model (one-way read)
  • \{ "$bindState": "/path" \} - two-way binding: use on the natural value prop (value, checked, pressed, etc.) of form components.
  • \{ "$bindItem": "field" \} - two-way binding to a repeat item field. Use inside repeat scopes.
  • \{ "$cond": &lt;condition&gt;, "$then": &lt;value&gt;, "$else": &lt;value&gt; \} - conditional value
{
  "type": "TextInput",
  "props": {
    "value": { "$bindState": "/form/email" },
    "placeholder": "Email"
  }
}

Components do not use a statePath prop for two-way binding. Use \{ "$bindState": "/path" \} on the natural value prop instead.

Built-in Actions

The setState action is handled automatically by ActionProvider and updates the state model directly, which re-evaluates visibility conditions and dynamic prop expressions:

{ "action": "setState", "actionParams": { "statePath": "/activeTab", "value": "home" } }

Providers

ProviderPurpose
StateProviderShare state across components (JSON Pointer paths). Accepts optional store prop for controlled mode.
ActionProviderHandle actions dispatched from components
VisibilityProviderEnable conditional rendering based on state
ValidationProviderForm field validation

External Store (Controlled Mode)

Pass a StateStore to StateProvider (or JSONUIProvider / createRenderer) to use external state management:

import { createStateStore, type StateStore } from "@json-render/react-native";

const store = createStateStore({ count: 0 });

<StateProvider store={store}>{children}</StateProvider>

store.set("/count", 1); // React re-renders automatically

When store is provided, initialState and onStateChange are ignored.

Key Exports

ExportPurpose
defineRegistryCreate a type-safe component registry from a catalog
RendererRender a spec using a registry
schemaReact Native element tree schema
standardComponentDefinitionsCatalog definitions for all standard components
standardActionDefinitionsCatalog definitions for standard actions
standardComponentsPre-built component implementations
createStandardActionHandlersCreate handlers for standard actions
useStateStoreAccess state context
useStateValueGet single value from state
useBoundPropTwo-way state binding via $bindState/$bindItem
useStateBinding(deprecated) Legacy two-way binding by path
useActionsAccess actions context
useActionGet a single action dispatch function
useUIStreamStream specs from an API endpoint
createStateStoreCreate a framework-agnostic in-memory StateStore
StateStoreInterface for plugging in external state management

Upstream React

<!-- SYNCED from vercel-labs/json-render (skills/react/SKILL.md) --> <!-- Hash: 9160db7dcdac2bb5be7201c79f43c834f972834671a640b412b426f4adaa7785 --> <!-- Re-sync: bash scripts/sync-vercel-skills.sh -->

@json-render/react

React renderer that converts JSON specs into React component trees.

Quick Start

import { defineRegistry, Renderer } from "@json-render/react";
import { catalog } from "./catalog";

const { registry } = defineRegistry(catalog, {
  components: {
    Card: ({ props, children }) => <div>{props.title}{children}</div>,
  },
});

function App({ spec }) {
  return <Renderer spec={spec} registry={registry} />;
}

Creating a Catalog

import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/react/schema";
import { defineRegistry } from "@json-render/react";
import { z } from "zod";

// Create catalog with props schemas
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",
    },
  },
});

// Define component implementations with type-safe props
const { registry } = defineRegistry(catalog, {
  components: {
    Button: ({ props }) => (
      <button className={props.variant}>{props.label}</button>
    ),
    Card: ({ props, children }) => (
      <div className="card">
        <h2>{props.title}</h2>
        {children}
      </div>
    ),
  },
});

Spec Structure (Element Tree)

The React schema uses an element tree format:

{
  "root": {
    "type": "Card",
    "props": { "title": "Hello" },
    "children": [
      { "type": "Button", "props": { "label": "Click me" } }
    ]
  }
}

Visibility Conditions

Use visible on elements to show/hide based on state. New syntax: \{ "$state": "/path" \}, \{ "$state": "/path", "eq": value \}, \{ "$state": "/path", "not": true \}, \{ "$and": [cond1, cond2] \} for AND, \{ "$or": [cond1, cond2] \} for OR. Helpers: visibility.when("/path"), visibility.unless("/path"), visibility.eq("/path", val), visibility.and(cond1, cond2), visibility.or(cond1, cond2).

Providers

ProviderPurpose
StateProviderShare state across components (JSON Pointer paths). Accepts optional store prop for controlled mode.
ActionProviderHandle actions dispatched via the event system
VisibilityProviderEnable conditional rendering based on state
ValidationProviderForm field validation

External Store (Controlled Mode)

Pass a StateStore to StateProvider (or JSONUIProvider / createRenderer) to use external state management (Redux, Zustand, XState, etc.):

import { createStateStore, type StateStore } from "@json-render/react";

const store = createStateStore({ count: 0 });

<StateProvider store={store}>{children}</StateProvider>

// Mutate from anywhere โ€” React re-renders automatically:
store.set("/count", 1);

When store is provided, initialState and onStateChange are ignored.

Dynamic Prop Expressions

Any prop value can be a data-driven expression resolved by the renderer before components receive props:

  • \{ "$state": "/state/key" \} - reads from state model (one-way read)
  • \{ "$bindState": "/path" \} - two-way binding: reads from state and enables write-back. Use on the natural value prop (value, checked, pressed, etc.) of form components.
  • \{ "$bindItem": "field" \} - two-way binding to a repeat item field. Use inside repeat scopes.
  • \{ "$cond": &lt;condition&gt;, "$then": &lt;value&gt;, "$else": &lt;value&gt; \} - conditional value
  • \{ "$template": "Hello, $\{/name\}!" \} - interpolates state values into strings
  • \{ "$computed": "fn", "args": \{ ... \} \} - calls registered functions with resolved args
{
  "type": "Input",
  "props": {
    "value": { "$bindState": "/form/email" },
    "placeholder": "Email"
  }
}

Components do not use a statePath prop for two-way binding. Use \{ "$bindState": "/path" \} on the natural value prop instead.

Components receive already-resolved props. For two-way bound props, use the useBoundProp hook with the bindings map the renderer provides.

Register $computed functions via the functions prop on JSONUIProvider or createRenderer:

<JSONUIProvider
  functions={{ fullName: (args) => `${args.first} ${args.last}` }}
>

Event System

Components use emit to fire named events, or on() to get an event handle with metadata. The element's on field maps events to action bindings:

// Simple event firing
Button: ({ props, emit }) => (
  <button onClick={() => emit("press")}>{props.label}</button>
),

// Event handle with metadata (e.g. preventDefault)
Link: ({ props, on }) => {
  const click = on("click");
  return (
    <a href={props.href} onClick={(e) => {
      if (click.shouldPreventDefault) e.preventDefault();
      click.emit();
    }}>{props.label}</a>
  );
},
{
  "type": "Button",
  "props": { "label": "Submit" },
  "on": { "press": { "action": "submit" } }
}

The EventHandle returned by on() has: emit(), shouldPreventDefault (boolean), and bound (boolean).

State Watchers

Elements can declare a watch field (top-level, sibling of type/props/children) to trigger actions when state values change:

{
  "type": "Select",
  "props": { "value": { "$bindState": "/form/country" }, "options": ["US", "Canada"] },
  "watch": { "/form/country": { "action": "loadCities" } },
  "children": []
}

Built-in Actions

The setState, pushState, removeState, and validateForm actions are built into the React schema and handled automatically by ActionProvider. They are injected into AI prompts without needing to be declared in catalog actions:

{ "action": "setState", "params": { "statePath": "/activeTab", "value": "home" } }
{ "action": "pushState", "params": { "statePath": "/items", "value": { "text": "New" } } }
{ "action": "removeState", "params": { "statePath": "/items", "index": 0 } }
{ "action": "validateForm", "params": { "statePath": "/formResult" } }

validateForm validates all registered fields and writes \{ valid, errors \} to state.

Note: statePath in action params (e.g. setState.statePath) targets the mutation path. Two-way binding in component props uses \{ "$bindState": "/path" \} on the value prop, not statePath.

useBoundProp

For form components that need two-way binding, use useBoundProp with the bindings map the renderer provides when a prop uses \{ "$bindState": "/path" \} or \{ "$bindItem": "field" \}:

import { useBoundProp } from "@json-render/react";

Input: ({ element, bindings }) => {
  const [value, setValue] = useBoundProp<string>(
    element.props.value,
    bindings?.value
  );
  return (
    <input
      value={value ?? ""}
      onChange={(e) => setValue(e.target.value)}
    />
  );
},

useBoundProp(propValue, bindingPath) returns [value, setValue]. The value is the resolved prop; setValue writes back to the bound state path (no-op if not bound).

BaseComponentProps

For building reusable component libraries not tied to a specific catalog (e.g. @json-render/shadcn):

import type { BaseComponentProps } from "@json-render/react";

const Card = ({ props, children }: BaseComponentProps<{ title?: string }>) => (
  <div>{props.title}{children}</div>
);

defineRegistry

defineRegistry conditionally requires the actions field only when the catalog declares actions. Catalogs with actions: \{\} can omit it.

Key Exports

ExportPurpose
defineRegistryCreate a type-safe component registry from a catalog
RendererRender a spec using a registry
schemaElement tree schema (includes built-in state actions: setState, pushState, removeState, validateForm)
useStateStoreAccess state context
useStateValueGet single value from state
useBoundPropTwo-way binding for $bindState/$bindItem expressions
useActionsAccess actions context
useActionGet a single action dispatch function
useOptionalValidationNon-throwing variant of useValidation (returns null if no provider)
useUIStreamStream specs from an API endpoint
createStateStoreCreate a framework-agnostic in-memory StateStore
StateStoreInterface for plugging in external state management
BaseComponentPropsCatalog-agnostic base type for reusable component libraries
EventHandleEvent handle type (emit, shouldPreventDefault, bound)
ComponentContextTyped component context (catalog-aware)

Upstream Redux

<!-- SYNCED from vercel-labs/json-render (skills/redux/SKILL.md) --> <!-- Hash: 7df1d68857ed6e11bbbef5e29999d9d82c0a0e21193f863a406fd98b0b6d29c0 --> <!-- Re-sync: bash scripts/sync-vercel-skills.sh -->

@json-render/redux

Redux adapter for json-render's StateStore interface. Wire a Redux store (or Redux Toolkit slice) as the state backend for json-render.

Installation

npm install @json-render/redux @json-render/core @json-render/react redux
# or with Redux Toolkit (recommended):
npm install @json-render/redux @json-render/core @json-render/react @reduxjs/toolkit

Usage

import { configureStore, createSlice } from "@reduxjs/toolkit";
import { reduxStateStore } from "@json-render/redux";
import { StateProvider } from "@json-render/react";

// 1. Define a slice for json-render state
const uiSlice = createSlice({
  name: "ui",
  initialState: { count: 0 } as Record<string, unknown>,
  reducers: {
    replaceUiState: (_state, action) => action.payload,
  },
});

// 2. Create the Redux store
const reduxStore = configureStore({
  reducer: { ui: uiSlice.reducer },
});

// 3. Create the json-render StateStore adapter
const store = reduxStateStore({
  store: reduxStore,
  selector: (state) => state.ui,
  dispatch: (next, s) => s.dispatch(uiSlice.actions.replaceUiState(next)),
});

// 4. Use it
<StateProvider store={store}>
  {/* json-render reads/writes go through Redux */}
</StateProvider>

API

reduxStateStore(options)

Creates a StateStore backed by a Redux store.

OptionTypeRequiredDescription
storeStoreYesThe Redux store instance
selector(state) => StateModelYesSelect the json-render slice from the Redux state tree. Use (s) => s if the entire state is the model.
dispatch(nextState, store) => voidYesDispatch an action that replaces the selected slice with the next state

The dispatch callback receives the full next state model and the Redux store.

Upstream Remotion

<!-- SYNCED from vercel-labs/json-render (skills/remotion/SKILL.md) --> <!-- Hash: 59d63ac06729795f0981fe0cfadf6c1c34454be0329a34f2d36c9be2391fbbe9 --> <!-- Re-sync: bash scripts/sync-vercel-skills.sh -->

@json-render/remotion

Remotion renderer that converts JSON timeline specs into video compositions.

Quick Start

import { Player } from "@remotion/player";
import { Renderer, type TimelineSpec } from "@json-render/remotion";

function VideoPlayer({ spec }: { spec: TimelineSpec }) {
  return (
    <Player
      component={Renderer}
      inputProps={{ spec }}
      durationInFrames={spec.composition.durationInFrames}
      fps={spec.composition.fps}
      compositionWidth={spec.composition.width}
      compositionHeight={spec.composition.height}
      controls
    />
  );
}

Using Standard Components

import { defineCatalog } from "@json-render/core";
import {
  schema,
  standardComponentDefinitions,
  standardTransitionDefinitions,
  standardEffectDefinitions,
} from "@json-render/remotion";

export const videoCatalog = defineCatalog(schema, {
  components: standardComponentDefinitions,
  transitions: standardTransitionDefinitions,
  effects: standardEffectDefinitions,
});

Adding Custom Components

import { z } from "zod";

const catalog = defineCatalog(schema, {
  components: {
    ...standardComponentDefinitions,
    MyCustomClip: {
      props: z.object({ text: z.string() }),
      type: "scene",
      defaultDuration: 90,
      description: "My custom video clip",
    },
  },
});

// Pass custom component to Renderer
<Player
  component={Renderer}
  inputProps={{
    spec,
    components: { MyCustomClip: MyCustomComponent },
  }}
/>

Timeline Spec Structure

{
  "composition": { "id": "video", "fps": 30, "width": 1920, "height": 1080, "durationInFrames": 300 },
  "tracks": [{ "id": "main", "name": "Main", "type": "video", "enabled": true }],
  "clips": [
    { "id": "clip-1", "trackId": "main", "component": "TitleCard", "props": { "title": "Hello" }, "from": 0, "durationInFrames": 90 }
  ],
  "audio": { "tracks": [] }
}

Standard Components

ComponentTypeDescription
TitleCardsceneFull-screen title with subtitle
TypingTextsceneTerminal-style typing animation
ImageSlideimageFull-screen image display
SplitScreensceneTwo-panel comparison
QuoteCardsceneQuote with attribution
StatCardsceneAnimated statistic display
TextOverlayoverlayText overlay
LowerThirdoverlayName/title overlay

Key Exports

ExportPurpose
RendererRender spec to Remotion composition
schemaTimeline schema
standardComponentsPre-built component registry
standardComponentDefinitionsCatalog definitions
useTransitionTransition animation hook
ClipWrapperWrap clips with transitions

Upstream Shadcn Svelte

<!-- SYNCED from vercel-labs/json-render (skills/shadcn-svelte/SKILL.md) --> <!-- Hash: f0029125daa312c5fd32c2ee105d31f87a0c67a371315f5d73c63486ec139e79 --> <!-- Re-sync: bash scripts/sync-vercel-skills.sh -->

@json-render/shadcn-svelte

Pre-built shadcn-svelte component definitions and implementations for json-render. Provides 36 components built on Svelte 5 + Tailwind CSS.

Two Entry Points

Entry PointExportsUse For
@json-render/shadcn-svelte/catalogshadcnComponentDefinitionsCatalog schemas (no Svelte dependency, safe for server)
@json-render/shadcn-svelteshadcnComponents, shadcnComponentDefinitionsSvelte implementations + catalog schemas

Usage Pattern

Pick the components you need from the standard definitions. Do not spread all definitions -- explicitly select what your app uses:

import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/svelte/schema";
import { shadcnComponentDefinitions } from "@json-render/shadcn-svelte/catalog";
import { defineRegistry } from "@json-render/svelte";
import { shadcnComponents } from "@json-render/shadcn-svelte";

// Catalog: pick definitions
const catalog = defineCatalog(schema, {
  components: {
    Card: shadcnComponentDefinitions.Card,
    Stack: shadcnComponentDefinitions.Stack,
    Heading: shadcnComponentDefinitions.Heading,
    Button: shadcnComponentDefinitions.Button,
    Input: shadcnComponentDefinitions.Input,
  },
  actions: {},
});

// Registry: pick matching implementations
const { registry } = defineRegistry(catalog, {
  components: {
    Card: shadcnComponents.Card,
    Stack: shadcnComponents.Stack,
    Heading: shadcnComponents.Heading,
    Button: shadcnComponents.Button,
    Input: shadcnComponents.Input,
  },
});

Then render in your Svelte component:

<script lang="ts">
  import { Renderer, JsonUIProvider } from "@json-render/svelte";

  export let spec;
  export let registry;
</script>

<JsonUIProvider initialState={spec?.state ?? {}}>
  <Renderer {spec} {registry} />
</JsonUIProvider>

Available Components

Layout

  • Card - Container with optional title, description, maxWidth, centered
  • Stack - Flex container with direction, gap, align, justify
  • Grid - Grid layout with columns (number) and gap
  • Separator - Visual divider with orientation
  • Tabs - Tabbed navigation with tabs array, defaultValue, value
  • Accordion - Collapsible sections with items array and type (single/multiple)
  • Collapsible - Single collapsible section with title
  • Pagination - Page navigation with totalPages and page

Overlay

  • Dialog - Modal dialog with title, description, openPath
  • Drawer - Bottom drawer with title, description, openPath
  • Tooltip - Hover tooltip with content and text
  • Popover - Click-triggered popover with trigger and content
  • DropdownMenu - Dropdown with label and items array

Content

  • Heading - Heading text with level (h1-h4)
  • Text - Paragraph with variant (body, caption, muted, lead, code)
  • Image - Image with alt, width, height
  • Avatar - User avatar with src, name, size
  • Badge - Status badge with text and variant (default, secondary, destructive, outline)
  • Alert - Alert banner with title, message, type (success, warning, info, error)
  • Carousel - Scrollable carousel with items array
  • Table - Data table with columns (string[]) and rows (string[][])

Feedback

  • Progress - Progress bar with value, max, label
  • Skeleton - Loading placeholder with width, height, rounded
  • Spinner - Loading spinner with size and label

Input

  • Button - Button with label, variant (primary, secondary, danger), disabled
  • Link - Anchor link with label and href
  • Input - Text input with label, name, type, placeholder, value, checks
  • Textarea - Multi-line input with label, name, placeholder, rows, value, checks
  • Select - Dropdown select with label, name, options (string[]), value, checks
  • Checkbox - Checkbox with label, name, checked, checks, validateOn
  • Radio - Radio group with label, name, options (string[]), value, checks, validateOn
  • Switch - Toggle switch with label, name, checked, checks, validateOn
  • Slider - Range slider with label, min, max, step, value
  • Toggle - Toggle button with label, pressed, variant
  • ToggleGroup - Group of toggles with items, type, value
  • ButtonGroup - Button group with buttons array and selected

Validation Timing (validateOn)

All form components support validateOn to control when validation runs:

  • "change" -- validate on every input change (default for Select, Checkbox, Radio, Switch)
  • "blur" -- validate when field loses focus (default for Input, Textarea)
  • "submit" -- validate only on form submission

Important Notes

  • The /catalog entry point has no Svelte dependency -- use it for server-side prompt generation
  • Components use Tailwind CSS classes -- your app must have Tailwind configured
  • Component implementations use bundled shadcn-svelte primitives (not your app's $lib/components/ui/)
  • All form inputs support checks for validation (type + message pairs) and validateOn for timing
  • Events: inputs emit change/submit/focus/blur; buttons emit press; selects emit change/select

Upstream Shadcn

<!-- SYNCED from vercel-labs/json-render (skills/shadcn/SKILL.md) --> <!-- Hash: d6f1591aa8055898ef65b0903083caf25ed43304c42c668e070e357a4ac95316 --> <!-- Re-sync: bash scripts/sync-vercel-skills.sh -->

@json-render/shadcn

Pre-built shadcn/ui component definitions and implementations for json-render. Provides 36 components built on Radix UI + Tailwind CSS.

Two Entry Points

Entry PointExportsUse For
@json-render/shadcn/catalogshadcnComponentDefinitionsCatalog schemas (no React dependency, safe for server)
@json-render/shadcnshadcnComponentsReact implementations

Usage Pattern

Pick the components you need from the standard definitions. Do not spread all definitions -- explicitly select what your app uses:

import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/react/schema";
import { shadcnComponentDefinitions } from "@json-render/shadcn/catalog";
import { defineRegistry } from "@json-render/react";
import { shadcnComponents } from "@json-render/shadcn";

// Catalog: pick definitions
const catalog = defineCatalog(schema, {
  components: {
    Card: shadcnComponentDefinitions.Card,
    Stack: shadcnComponentDefinitions.Stack,
    Heading: shadcnComponentDefinitions.Heading,
    Button: shadcnComponentDefinitions.Button,
    Input: shadcnComponentDefinitions.Input,
  },
  actions: {},
});

// Registry: pick matching implementations
const { registry } = defineRegistry(catalog, {
  components: {
    Card: shadcnComponents.Card,
    Stack: shadcnComponents.Stack,
    Heading: shadcnComponents.Heading,
    Button: shadcnComponents.Button,
    Input: shadcnComponents.Input,
  },
});

State actions (setState, pushState, removeState) are built into the React schema and handled by ActionProvider automatically. No need to declare them.

Extending with Custom Components

Add custom components alongside standard ones:

const catalog = defineCatalog(schema, {
  components: {
    // Standard
    Card: shadcnComponentDefinitions.Card,
    Stack: shadcnComponentDefinitions.Stack,

    // Custom
    Metric: {
      props: z.object({
        label: z.string(),
        value: z.string(),
        trend: z.enum(["up", "down", "neutral"]).nullable(),
      }),
      description: "KPI metric display",
    },
  },
  actions: {},
});

const { registry } = defineRegistry(catalog, {
  components: {
    Card: shadcnComponents.Card,
    Stack: shadcnComponents.Stack,
    Metric: ({ props }) => <div>{props.label}: {props.value}</div>,
  },
});

Available Components

Layout

  • Card - Container with optional title, description, maxWidth, centered
  • Stack - Flex container with direction, gap, align, justify
  • Grid - Grid layout with columns (number) and gap
  • Separator - Visual divider with orientation
  • Tabs - Tabbed navigation with tabs array, defaultValue, value
  • Accordion - Collapsible sections with items array and type (single/multiple)
  • Collapsible - Single collapsible section with title
  • Pagination - Page navigation with totalPages and page

Overlay

  • Dialog - Modal dialog with title, description, openPath
  • Drawer - Bottom drawer with title, description, openPath
  • Tooltip - Hover tooltip with content and text
  • Popover - Click-triggered popover with trigger and content
  • DropdownMenu - Dropdown with label and items array

Content

  • Heading - Heading text with level (h1-h4)
  • Text - Paragraph with variant (body, caption, muted, lead, code)
  • Image - Image with alt, width, height
  • Avatar - User avatar with src, name, size
  • Badge - Status badge with text and variant (default, secondary, destructive, outline)
  • Alert - Alert banner with title, message, type (success, warning, info, error)
  • Carousel - Scrollable carousel with items array
  • Table - Data table with columns (string[]) and rows (string[][])

Feedback

  • Progress - Progress bar with value, max, label
  • Skeleton - Loading placeholder with width, height, rounded
  • Spinner - Loading spinner with size and label

Input

  • Button - Button with label, variant (primary, secondary, danger), disabled
  • Link - Anchor link with label and href
  • Input - Text input with label, name, type, placeholder, value, checks
  • Textarea - Multi-line input with label, name, placeholder, rows, value, checks
  • Select - Dropdown select with label, name, options (string[]), value, checks
  • Checkbox - Checkbox with label, name, checked, checks, validateOn
  • Radio - Radio group with label, name, options (string[]), value, checks, validateOn
  • Switch - Toggle switch with label, name, checked, checks, validateOn
  • Slider - Range slider with label, min, max, step, value
  • Toggle - Toggle button with label, pressed, variant
  • ToggleGroup - Group of toggles with items, type, value
  • ButtonGroup - Button group with buttons array and selected

Built-in Actions (from @json-render/react)

These are built into the React schema and handled by ActionProvider automatically. They appear in prompts without needing to be declared in the catalog.

  • setState - Set a value at a state path (\{ statePath, value \})
  • pushState - Push a value onto an array (\{ statePath, value, clearStatePath? \})
  • removeState - Remove an array item by index (\{ statePath, index \})
  • validateForm - Validate all fields, write \{ valid, errors \} to state (\{ statePath? \})

Validation Timing (validateOn)

All form components support validateOn to control when validation runs:

  • "change" โ€” validate on every input change (default for Select, Checkbox, Radio, Switch)
  • "blur" โ€” validate when field loses focus (default for Input, Textarea)
  • "submit" โ€” validate only on form submission

Important Notes

  • The /catalog entry point has no React dependency -- use it for server-side prompt generation
  • Components use Tailwind CSS classes -- your app must have Tailwind configured
  • Component implementations use bundled shadcn/ui primitives (not your app's components/ui/)
  • All form inputs support checks for validation (type + message pairs) and validateOn for timing
  • Events: inputs emit change/submit/focus/blur; buttons emit press; selects emit change/select

Upstream Solid

<!-- SYNCED from vercel-labs/json-render (skills/solid/SKILL.md) --> <!-- Hash: f94634c558d7d9fb323ba61bd9c8b08b1663260172bb9152b13f6af9b8296542 --> <!-- Re-sync: bash scripts/sync-vercel-skills.sh -->

@json-render/solid

@json-render/solid renders json-render specs into Solid component trees with fine-grained reactivity.

Quick Start

import { Renderer, JSONUIProvider } from "@json-render/solid";
import type { Spec } from "@json-render/solid";
import { registry } from "./registry";

export function App(props: { spec: Spec | null }) {
  return (
    <JSONUIProvider registry={registry} initialState={{}}>
      <Renderer spec={props.spec} registry={registry} />
    </JSONUIProvider>
  );
}

Create a Catalog

import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/solid/schema";
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",
    },
  },
  actions: {
    submit: { description: "Submit data" },
  },
});

Define Components

Components receive ComponentRenderProps from the renderer:

interface ComponentRenderProps<P = Record<string, unknown>> {
  element: UIElement<string, P>;
  children?: JSX.Element;
  emit: (event: string) => void;
  on: (event: string) => EventHandle;
  bindings?: Record<string, string>;
  loading?: boolean;
}

Example:

import type { BaseComponentProps } from "@json-render/solid";

export function Button(props: BaseComponentProps<{ label: string }>) {
  return (
    <button onClick={() => props.emit("press")}>{props.props.label}</button>
  );
}

Create a Registry

import { defineRegistry } from "@json-render/solid";
import { catalog } from "./catalog";
import { Card } from "./Card";
import { Button } from "./Button";

const { registry, handlers, executeAction } = defineRegistry(catalog, {
  components: {
    Card,
    Button,
  },
  actions: {
    submit: async (params, setState, state) => {
      // custom action logic
    },
  },
});

Spec Structure

{
  "root": "card1",
  "elements": {
    "card1": {
      "type": "Card",
      "props": { "title": "Hello" },
      "children": ["btn1"]
    },
    "btn1": {
      "type": "Button",
      "props": { "label": "Click me" },
      "on": {
        "press": { "action": "submit" }
      }
    }
  }
}

Providers

  • StateProvider: state model read/write and controlled mode via store
  • VisibilityProvider: evaluates visible conditions
  • ValidationProvider: field validation + validateForm integration
  • ActionProvider: runs built-in and custom actions
  • JSONUIProvider: combined provider wrapper

Hooks

  • useStateStore, useStateValue, useStateBinding
  • useVisibility, useIsVisible
  • useActions, useAction
  • useValidation, useOptionalValidation, useFieldValidation
  • useBoundProp
  • useUIStream, useChatUI

Built-in Actions

Handled automatically by ActionProvider:

  • setState
  • pushState
  • removeState
  • validateForm

Dynamic Props and Bindings

Supported expression forms include:

  • \{"$state": "/path"\}
  • \{"$bindState": "/path"\}
  • \{"$bindItem": "field"\}
  • \{"$template": "Hi $\{/user/name\}"\}
  • \{"$computed": "fn", "args": \{...\}\}
  • \{"$cond": &lt;condition&gt;, "$then": &lt;value&gt;, "$else": &lt;value&gt;\}

Use useBoundProp in components for writable bound values:

import { useBoundProp } from "@json-render/solid";

function Input(props: BaseComponentProps<{ value?: string }>) {
  const [value, setValue] = useBoundProp(
    props.props.value,
    props.bindings?.value,
  );
  return (
    <input
      value={String(value() ?? "")}
      onInput={(e) => setValue(e.currentTarget.value)}
    />
  );
}

useStateValue, useStateBinding, and the state / errors / isValid fields from useFieldValidation are reactive accessors in Solid. Call them as functions inside JSX, createMemo, or createEffect.

Solid Reactivity Rules

  • Do not destructure component props in function signatures when values need to stay reactive.
  • Keep changing reads inside JSX expressions, createMemo, or createEffect.
  • Context values are exposed through getter-based objects so consumers always observe live signals.

Streaming UI

import { useUIStream, Renderer } from "@json-render/solid";

const stream = useUIStream({ api: "/api/generate-ui" });
await stream.send("Create a support dashboard");

<Renderer
  spec={stream.spec}
  registry={registry}
  loading={stream.isStreaming}
/>;

Use useChatUI for chat + UI generation flows.

Upstream Svelte

<!-- SYNCED from vercel-labs/json-render (skills/svelte/SKILL.md) --> <!-- Hash: 5aced90dba057566833f53cd5c5ea25734650bbd5d160ec6d11517d8f0c8807e --> <!-- Re-sync: bash scripts/sync-vercel-skills.sh -->

@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&lt;TProps&gt;:

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:

  • \{ "$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:

ContextPurpose
StateContextShare state across components (JSON Pointer paths)
ActionContextHandle actions dispatched via the event system
VisibilityContextEnable conditional rendering based on state
ValidationContextForm 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:

  • \{"$state": "/state/key"\} - read from state
  • \{"$bindState": "/form/email"\} - read + write-back to state
  • \{"$bindItem": "field"\} - read + write-back for repeat items
  • \{"$cond": &lt;condition&gt;, "$then": &lt;value&gt;, "$else": &lt;value&gt;\} - conditional 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:

  • 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:

<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");

Upstream Vue

<!-- SYNCED from vercel-labs/json-render (skills/vue/SKILL.md) --> <!-- Hash: a501e60dd4104dab555b76021b39d8fbf8014dabcf09dd57477fb56b647a3e76 --> <!-- Re-sync: bash scripts/sync-vercel-skills.sh -->

@json-render/vue

Vue 3 renderer that converts JSON specs into Vue component trees with data binding, visibility, and actions.

Installation

npm install @json-render/vue @json-render/core zod

Peer dependencies: vue ^3.5.0 and zod ^4.0.0.

Quick Start

Create a Catalog

import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/vue/schema";
import { z } from "zod";

export const catalog = defineCatalog(schema, {
  components: {
    Card: {
      props: z.object({ title: z.string(), description: z.string().nullable() }),
      description: "A card container",
    },
    Button: {
      props: z.object({ label: z.string(), action: z.string() }),
      description: "A clickable button",
    },
  },
  actions: {},
});

Define Registry with h() Render Functions

import { h } from "vue";
import { defineRegistry } from "@json-render/vue";
import { catalog } from "./catalog";

export const { registry } = defineRegistry(catalog, {
  components: {
    Card: ({ props, children }) =>
      h("div", { class: "card" }, [
        h("h3", null, props.title),
        props.description ? h("p", null, props.description) : null,
        children,
      ]),
    Button: ({ props, emit }) =>
      h("button", { onClick: () => emit("press") }, props.label),
  },
});

Render Specs

<script setup lang="ts">
import { StateProvider, ActionProvider, Renderer } from "@json-render/vue";
import { registry } from "./registry";

const spec = { root: "card-1", elements: { /* ... */ } };
</script>

<template>
  <StateProvider :initial-state="{ form: { name: '' } }">
    <ActionProvider :handlers="{ submit: handleSubmit }">
      <Renderer :spec="spec" :registry="registry" />
    </ActionProvider>
  </StateProvider>
</template>

Providers

ProviderPurpose
StateProviderShare state across components (JSON Pointer paths). Accepts initialState or store for controlled mode.
ActionProviderHandle actions dispatched via the event system
VisibilityProviderEnable conditional rendering based on state
ValidationProviderForm field validation

Composables

ComposablePurpose
useStateStore()Access state context (state as ShallowRef, get, set, update)
useStateValue(path)Get single value from state
useIsVisible(condition)Check if a visibility condition is met
useActions()Access action context
useAction(binding)Get a single action dispatch function
useFieldValidation(path, config)Field validation state
useBoundProp(propValue, bindingPath)Two-way binding for $bindState/$bindItem

Note: useStateStore().state returns a ShallowRef&lt;StateModel&gt; โ€” use state.value to access.

External Store (StateStore)

Pass a StateStore to StateProvider to wire json-render to Pinia, VueUse, or any state management:

import { createStateStore, type StateStore } from "@json-render/vue";

const store = createStateStore({ count: 0 });
<StateProvider :store="store">
  <Renderer :spec="spec" :registry="registry" />
</StateProvider>

Dynamic Prop Expressions

Props support $state, $bindState, $cond, $template, $computed. Use \{ "$bindState": "/path" \} on the natural value prop for two-way binding.

Visibility Conditions

{ "$state": "/user/isAdmin" }
{ "$state": "/status", "eq": "active" }
{ "$state": "/maintenance", "not": true }
[ cond1, cond2 ]  // implicit AND

Built-in Actions

setState, pushState, removeState, and validateForm are built into the Vue schema and handled by ActionProvider:

{
  "action": "setState",
  "params": { "statePath": "/activeTab", "value": "settings" }
}

Event System

Components use emit(event) to fire events, or on(event) for metadata (shouldPreventDefault, bound).

Streaming

useUIStream and useChatUI return Vue Refs for streaming specs from an API.

BaseComponentProps

For catalog-agnostic reusable components:

import type { BaseComponentProps } from "@json-render/vue";

const Card = ({ props, children }: BaseComponentProps<{ title?: string }>) =>
  h("div", null, [props.title, children]);

Key Exports

ExportPurpose
defineRegistryCreate a type-safe component registry from a catalog
RendererRender a spec using a registry
schemaElement tree schema (from @json-render/vue/schema)
StateProvider, ActionProvider, VisibilityProvider, ValidationProviderContext providers
useStateStore, useStateValue, useBoundPropState composables
useActions, useActionAction composables
useFieldValidation, useIsVisibleValidation and visibility
useUIStream, useChatUIStreaming composables
createStateStoreCreate in-memory StateStore
BaseComponentPropsCatalog-agnostic component props type

Upstream Xstate

<!-- SYNCED from vercel-labs/json-render (skills/xstate/SKILL.md) --> <!-- Hash: f69e656f8e72e5f3225aa9e5b68304b1a4acf85cbc59c17a4b9f4c595b76d7bb --> <!-- Re-sync: bash scripts/sync-vercel-skills.sh -->

@json-render/xstate

XState Store adapter for json-render's StateStore interface. Wire an @xstate/store atom as the state backend for json-render.

Installation

npm install @json-render/xstate @json-render/core @json-render/react @xstate/store

Requires @xstate/store v3+.

Usage

import { createAtom } from "@xstate/store";
import { xstateStoreStateStore } from "@json-render/xstate";
import { StateProvider } from "@json-render/react";

// 1. Create an atom
const uiAtom = createAtom({ count: 0 });

// 2. Create the json-render StateStore adapter
const store = xstateStoreStateStore({ atom: uiAtom });

// 3. Use it
<StateProvider store={store}>
  {/* json-render reads/writes go through @xstate/store */}
</StateProvider>

API

xstateStoreStateStore(options)

Creates a StateStore backed by an @xstate/store atom.

OptionTypeRequiredDescription
atomAtom&lt;StateModel&gt;YesAn @xstate/store atom (from createAtom) holding the json-render state model

Upstream Yaml

<!-- SYNCED from vercel-labs/json-render (skills/yaml/SKILL.md) --> <!-- Hash: 7fbf15795ce16be3379ca2f62a0f7be103162ab9996392eb878441b70f1c0621 --> <!-- Re-sync: bash scripts/sync-vercel-skills.sh -->

@json-render/yaml

YAML wire format for @json-render/core. Progressive rendering and surgical edits via streaming YAML.

Key Concepts

  • YAML wire format: Alternative to JSONL that uses code fences (yaml-spec, yaml-edit, yaml-patch, diff)
  • Streaming parser: Incrementally parses YAML, emits JSON Patch operations via diffing
  • Edit modes: Patch (RFC 6902), merge (RFC 7396), and unified diff
  • AI SDK transform: TransformStream that converts YAML fences into json-render patches

Generating YAML Prompts

import { yamlPrompt } from "@json-render/yaml";
import { catalog } from "./catalog";

// Standalone mode (LLM outputs only YAML)
const systemPrompt = yamlPrompt(catalog, {
  mode: "standalone",
  editModes: ["merge"],
  customRules: ["Always use dark theme"],
});

// Inline mode (LLM responds conversationally, wraps YAML in fences)
const chatPrompt = yamlPrompt(catalog, { mode: "inline" });

Options:

  • system (string) โ€” Custom system message intro
  • mode ("standalone" | "inline") โ€” Output mode, default "standalone"
  • customRules (string[]) โ€” Additional rules appended to prompt
  • editModes (EditMode[]) โ€” Edit modes to document, default ["merge"]

AI SDK Transform

Use pipeYamlRender as a drop-in replacement for pipeJsonRender:

import { pipeYamlRender } from "@json-render/yaml";
import { createUIMessageStream, createUIMessageStreamResponse } from "ai";

const stream = createUIMessageStream({
  execute: async ({ writer }) => {
    writer.merge(pipeYamlRender(result.toUIMessageStream()));
  },
});
return createUIMessageStreamResponse({ stream });

For multi-turn edits, pass the previous spec:

pipeYamlRender(result.toUIMessageStream(), {
  previousSpec: currentSpec,
});

The transform recognizes four fence types:

  • yaml-spec โ€” Full spec, parsed progressively line-by-line
  • yaml-edit โ€” Partial YAML deep-merged with current spec (RFC 7396)
  • yaml-patch โ€” RFC 6902 JSON Patch lines
  • diff โ€” Unified diff applied to serialized spec

Streaming Parser (Low-Level)

import { createYamlStreamCompiler } from "@json-render/yaml";

const compiler = createYamlStreamCompiler<Spec>();

// Feed chunks as they arrive from any source
const { result, newPatches } = compiler.push("root: main\n");
compiler.push("elements:\n  main:\n    type: Card\n");

// Flush remaining data at end of stream
const { result: final } = compiler.flush();

// Reset for next stream (optionally with initial state)
compiler.reset({ root: "main", elements: {} });

Methods: push(chunk), flush(), getResult(), getPatches(), reset(initial?)

Edit Modes (from @json-render/core)

The YAML package uses the universal edit mode system from core:

import { buildEditInstructions, buildEditUserPrompt } from "@json-render/core";
import type { EditMode } from "@json-render/core";

// Generate edit instructions for YAML format
const instructions = buildEditInstructions({ modes: ["merge", "patch"] }, "yaml");

// Build user prompt with current spec context
const userPrompt = buildEditUserPrompt({
  prompt: "Change the title to Dashboard",
  currentSpec: spec,
  config: { modes: ["merge"] },
  format: "yaml",
  serializer: (s) => yamlStringify(s, { indent: 2 }).trimEnd(),
});

Fence Constants

For custom parsing, use the exported constants:

import {
  YAML_SPEC_FENCE,   // "```yaml-spec"
  YAML_EDIT_FENCE,   // "```yaml-edit"
  YAML_PATCH_FENCE,  // "```yaml-patch"
  DIFF_FENCE,        // "```diff"
  FENCE_CLOSE,       // "```"
} from "@json-render/yaml";

Key Exports

ExportDescription
yamlPromptGenerate YAML system prompt from catalog
createYamlTransformAI SDK TransformStream for YAML fences
pipeYamlRenderConvenience pipe wrapper (replaces pipeJsonRender)
createYamlStreamCompilerStreaming YAML parser with patch emission
YAML_SPEC_FENCEFence constant for yaml-spec
YAML_EDIT_FENCEFence constant for yaml-edit
YAML_PATCH_FENCEFence constant for yaml-patch
DIFF_FENCEFence constant for diff
FENCE_CLOSEFence close constant
diffToPatchesRe-export: object diff to JSON Patch
deepMergeSpecRe-export: RFC 7396 deep merge

Upstream Zustand

<!-- SYNCED from vercel-labs/json-render (skills/zustand/SKILL.md) --> <!-- Hash: 5ad0665e480b5e2a926626b82dbc89388801879503bdc1ceb152346692ddc502 --> <!-- Re-sync: bash scripts/sync-vercel-skills.sh -->

@json-render/zustand

Zustand adapter for json-render's StateStore interface. Wire a Zustand vanilla store as the state backend for json-render.

Installation

npm install @json-render/zustand @json-render/core @json-render/react zustand

Requires Zustand v5+. Zustand v4 is not supported due to breaking API changes in the vanilla store interface.

Usage

import { createStore } from "zustand/vanilla";
import { zustandStateStore } from "@json-render/zustand";
import { StateProvider } from "@json-render/react";

// 1. Create a Zustand vanilla store
const bearStore = createStore(() => ({
  count: 0,
  name: "Bear",
}));

// 2. Create the json-render StateStore adapter
const store = zustandStateStore({ store: bearStore });

// 3. Use it
<StateProvider store={store}>
  {/* json-render reads/writes go through Zustand */}
</StateProvider>

With a Nested Slice

const appStore = createStore(() => ({
  ui: { count: 0 },
  auth: { token: null },
}));

const store = zustandStateStore({
  store: appStore,
  selector: (s) => s.ui,
  updater: (next, s) => s.setState({ ui: next }),
});

API

zustandStateStore(options)

Creates a StateStore backed by a Zustand store.

OptionTypeRequiredDescription
storeStoreApi&lt;S&gt;YesZustand vanilla store (from createStore in zustand/vanilla)
selector(state) => StateModelNoSelect the json-render slice. Defaults to entire state.
updater(nextState, store) => voidNoApply next state to the store. Defaults to shallow merge. Override for nested slices, or use (next, s) => s.setState(next, true) for full replacement.
Edit on GitHub

Last updated on

On this page

json-render Component CatalogsStorybook โ†’ catalog import (#1529, 2026-04)New in 2026-04 (json-render 0.14 โ†’ 0.18)Quick ReferenceHow json-render WorksQuick Start โ€” 3 StepsStep 1: Define a CatalogLLM Structured Output CompatibilityStep 2: Implement ComponentsStep 3: Render a SpecSpec FormatWith Interactivity (on / watch / state)YAML Mode โ€” 30% Fewer TokensProgressive Streaming@json-render/shadcn โ€” 36 Pre-Built ComponentsStyle-Aware CatalogsEdit Modes โ€” patch / merge / diff (0.14+)Package EcosystemWhen to Use vs When NOT to UseMigrating from Custom GenUIRule DetailsCatalog DefinitionProp Constraintsshadcn CatalogToken OptimizationActions & StateKey DecisionsCommon MistakesRelated SkillsRules (5)Actions and State Bindings in Specs โ€” MEDIUMActions and State Bindings in SpecsThe Three PrimitivesBuilt-in ActionsState AdaptersCatalog Definition with defineCatalog and Zod โ€” HIGHCatalog Definition with defineCatalog and ZodChildren TypesMerging CatalogsProp Constraints for AI Safety โ€” HIGHProp Constraints for AI SafetyConstraint Patterns by Prop TypeRefinements for Complex Validationshadcn Catalog โ€” 29 Pre-Built Components โ€” MEDIUMshadcn Catalog โ€” 29 Pre-Built ComponentsThe 29 shadcn ComponentsWhen to Extend vs Use As-IsStyle-Aware OverridesToken Optimization โ€” YAML Mode โ€” MEDIUMToken Optimization โ€” YAML ModeFormat Selection CriteriaDecision RuleToken Comparison ExampleReferences (25)Migrating from Custom GenUI to json-renderMigrating from Custom GenUI to json-renderCommon Custom GenUI PatternsPattern A: Direct Type MappingPattern B: Switch StatementPattern C: Nested JSONMigration StepsStep 1: Inventory Existing ComponentsStep 2: Define Zod Schemas for Each ComponentStep 3: Flatten Nested SpecsStep 4: Replace Event HandlersStep 5: Wrap Existing Component ImplementationsStep 6: Update AI PromptsMigration Checklistjson-render Package Ecosystemjson-render Package EcosystemFoundationWeb RenderersComponent LibrariesMobileOutput Renderers3DMCP (Model Context Protocol)Code GenerationState AdaptersInstallation PatternsWrite Once, Render Anywherejson-render Spec Formatjson-render Spec FormatStructureFieldsroot (required)elements (required)Element Fieldsstate (optional)Flat-Tree DesignComplete ExampleValidation FlowStorybook โ†’ json-render catalog import โ€” HIGHStorybook โ†’ json-render Catalog ImportWorkflowStorybook ArgType โ†’ Zod mappingOutputcatalog.tscomponents.tsxValidationGenui-architect integrationWhy this mattersUpstream Core@json-render/coreKey ConceptsDefining a SchemaCreating a CatalogGenerating AI PromptsSpecStream UtilitiesDynamic Prop ExpressionsState WatchersValidationUser Prompt BuilderSpec ValidationVisibility ConditionsBuilt-in Actions in SchemaStateStoreKey ExportsUpstream Email@json-render/react-emailQuick StartSpec Structure (Element Tree)Creating a Catalog and RegistryServer-Side Render APIsVisibility and StateServer-Safe ImportKey ExportsSub-path ExportsStandard ComponentsDocument structureLayoutContentUtilityEmail Best PracticesUpstream Image@json-render/imageQuick StartUsing Standard ComponentsAdding Custom ComponentsStandard ComponentsKey ExportsSub-path ExportsUpstream Ink@json-render/inkQuick StartSpec Structure (Flat Element Map)Standard ComponentsLayoutContentInteractiveVisibility ConditionsDynamic Prop ExpressionsEvent SystemBuilt-in ActionsRepeat (Dynamic Lists)StreamingServer-Side Prompt GenerationProvidersExternal Store (Controlled Mode)createRenderer (Higher-Level API)Key ExportsTerminal UI Design GuidelinesUpstream Jotai@json-render/jotaiInstallationUsageWith a Shared Jotai StoreAPIjotaiStateStore(options)Upstream Mcp@json-render/mcpQuick StartServer (Node.js)Client (React, inside iframe)ArchitectureServer APIClient API (@json-render/mcp/app)Building the iframe HTMLClient ConfigurationCursor (.cursor/mcp.json)Claude DesktopDependenciesUpstream Next@json-render/nextQuick Start1. Define Your Spec2. Create the App3. Wire Up Route FilesKey ConceptsNextAppSpecRoute PatternsLayoutsBuilt-in ComponentsBuilt-in ActionsData LoadersSSREntry PointsAPI ReferenceServer Exports (@json-render/next/server)Client Exports (@json-render/next)Upstream Pdf@json-render/react-pdfInstallationQuick StartRender APIsStandard ComponentsCustom CatalogExternal Store (Controlled Mode)Server-Safe ImportUpstream R3f@json-render/react-three-fiberTwo Entry PointsUsage PatternRenderingThreeCanvas (convenience wrapper)Manual Canvas setupAvailable Components (19)Primitives (7)Lights (4)Other (8)Shared SchemasMaterial SchemaSpec FormatDependenciesUpstream React Native@json-render/react-nativeQuick StartStandard ComponentsLayoutContentInputFeedbackCompositeVisibility ConditionsPressable + setState PatternDynamic Prop ExpressionsBuilt-in ActionsProvidersExternal Store (Controlled Mode)Key ExportsUpstream React@json-render/reactQuick StartCreating a CatalogSpec Structure (Element Tree)Visibility ConditionsProvidersExternal Store (Controlled Mode)Dynamic Prop ExpressionsEvent SystemState WatchersBuilt-in ActionsuseBoundPropBaseComponentPropsdefineRegistryKey ExportsUpstream Redux@json-render/reduxInstallationUsageAPIreduxStateStore(options)Upstream Remotion@json-render/remotionQuick StartUsing Standard ComponentsAdding Custom ComponentsTimeline Spec StructureStandard ComponentsKey ExportsUpstream Shadcn Svelte@json-render/shadcn-svelteTwo Entry PointsUsage PatternAvailable ComponentsLayoutNavigationOverlayContentFeedbackInputValidation Timing (validateOn)Important NotesUpstream Shadcn@json-render/shadcnTwo Entry PointsUsage PatternExtending with Custom ComponentsAvailable ComponentsLayoutNavigationOverlayContentFeedbackInputBuilt-in Actions (from @json-render/react)Validation Timing (validateOn)Important NotesUpstream Solid@json-render/solidQuick StartCreate a CatalogDefine ComponentsCreate a RegistrySpec StructureProvidersHooksBuilt-in ActionsDynamic Props and BindingsSolid Reactivity RulesStreaming UIUpstream Svelte@json-render/svelteQuick StartCreating a CatalogDefining ComponentsCreating a RegistrySpec Structure (Element Tree)Visibility ConditionsProviders (via JsonUIProvider)Event SystemBuilt-in ActionsDynamic Props and Two-Way BindingContext HelpersStreaming UIUpstream Vue@json-render/vueInstallationQuick StartCreate a CatalogDefine Registry with h() Render FunctionsRender SpecsProvidersComposablesExternal Store (StateStore)Dynamic Prop ExpressionsVisibility ConditionsBuilt-in ActionsEvent SystemStreamingBaseComponentPropsKey ExportsUpstream Xstate@json-render/xstateInstallationUsageAPIxstateStoreStateStore(options)Upstream Yaml@json-render/yamlKey ConceptsGenerating YAML PromptsAI SDK TransformStreaming Parser (Low-Level)Edit Modes (from @json-render/core)Fence ConstantsKey ExportsUpstream Zustand@json-render/zustandInstallationUsageWith a Nested SliceAPIzustandStateStore(options)