JSON Render
Render DinachiUI interfaces from JSON specs. Define layouts, forms, and interactive UIs as data — ideal for AI-generated interfaces.
#Overview
JSON Render lets you describe DinachiUI interfaces as JSON and render them as fully accessible components. Write a spec — a flat map of elements with types, props, children, state, and events — and Dinachi renders it.
This is how the Playground works: an AI model outputs a JSON spec, Dinachi renders it live.
npx @dinachi/cli@latest add json-renderInstalls the adapter to lib/json-render/, all required UI components, and the @json-render/core, @json-render/react, and zod dependencies.
#Spec Format
A spec is a JSON object with three parts:
{
"root": "myCard",
"state": {
"form": { "name": "" }
},
"elements": {
"myCard": {
"type": "Card",
"props": { "title": "Hello" },
"children": ["nameInput", "submitBtn"]
},
"nameInput": {
"type": "Input",
"props": {
"label": "Name",
"value": { "$bindState": "/form/name" }
}
},
"submitBtn": {
"type": "Button",
"props": { "label": "Save" },
"on": {
"press": {
"action": "showToast",
"params": { "title": "Saved!" }
}
}
}
}
}| Field | Description |
|---|---|
root | ID of the top-level element to render |
state | Initial state object. Input components bind to paths within this via $bindState. |
elements | Flat map of element ID → element definition |
Each element has:
| Field | Description |
|---|---|
type | Component name ("Card", "Input", "Box", etc.) |
props | Component props (see reference below) |
children | Array of element IDs rendered inside this element (for container components) |
on | Event → action bindings (e.g., { "press": { "action": "navigate", "params": { "url": "/about" } } }) |
#Rendering a Spec
"use client";
import { Renderer, JSONUIProvider } from "@json-render/react";
import { registry, toastManager } from "@/lib/json-render";
import { Toast } from "@/components/ui/toast";
import type { Spec } from "@json-render/react";
export function GenerativeUI({ spec }: { spec: Spec }) {
return (
<JSONUIProvider registry={registry} initialState={spec.state ?? {}}>
<Renderer spec={spec} registry={registry} />
<Toast toastManager={toastManager} />
</JSONUIProvider>
);
}#State Binding
Use $bindState with a JSON Pointer path to create two-way bindings between components and the spec's state:
{
"state": { "settings": { "theme": "light", "notifications": true } },
"elements": {
"themeSelect": {
"type": "Select",
"props": {
"label": "Theme",
"options": [
{ "label": "Light", "value": "light" },
{ "label": "Dark", "value": "dark" }
],
"value": { "$bindState": "/settings/theme" }
}
},
"notifSwitch": {
"type": "Switch",
"props": {
"label": "Enable notifications",
"checked": { "$bindState": "/settings/notifications" }
}
}
}
}The bound prop name depends on the component — value for text inputs/selects, checked for Checkbox/Switch, pressed for Toggle, open for Dialog/Drawer/AlertDialog.
#Events and Actions
Components emit events (like press, change, submit, cancel, confirm). Bind them to actions in the on field:
{
"type": "Button",
"props": { "label": "Delete", "variant": "destructive" },
"on": {
"press": {
"action": "showToast",
"params": { "title": "Deleted", "variant": "error" }
}
}
}#Available Actions
| Action | Params | Description |
|---|---|---|
navigate | url, target? | Go to a URL. target: "_blank" opens a new tab. |
submit | formId? | Call requestSubmit() on a form element by ID. |
showToast | title, description?, variant?, timeout? | Show a toast. Variants: default, success, error, warning. |
#Events by Component
| Event | Emitted by |
|---|---|
press | Button |
change | Input, Textarea, Checkbox, Switch, Radio, Select, Slider, Toggle, NumberField, ToggleGroup, Tabs |
submit | Input (on Enter key) |
cancel | AlertDialog (cancel button) |
confirm | AlertDialog (action button) |
#Validation
Form components support validation via checks and validateOn:
{
"type": "Input",
"props": {
"label": "Email",
"type": "email",
"value": { "$bindState": "/form/email" },
"checks": [
{ "type": "required", "message": "Email is required" },
{ "type": "email", "message": "Enter a valid email" }
],
"validateOn": "blur"
}
}validateOn | Behavior |
|---|---|
"change" | Validates on every keystroke |
"blur" | Validates when field loses focus (default for Input, Textarea) |
"submit" | Validates on form submission |
Components that support checks/validateOn: Input, Textarea, Checkbox, Radio, Select, NumberField.
#Component Reference
For the full list of available components and their props, see the Component Reference.
#Full Example
A contact form with validation, state binding, and a toast on submit:
{
"root": "page",
"state": {
"form": { "name": "", "email": "", "message": "" }
},
"elements": {
"page": {
"type": "Box",
"props": { "gap": "lg", "padding": "lg" },
"children": ["heading", "card"]
},
"heading": {
"type": "Text",
"props": { "content": "Contact Us", "variant": "h2" }
},
"card": {
"type": "Card",
"props": { "title": "Send a Message" },
"children": ["formFields", "submitBtn"]
},
"formFields": {
"type": "Box",
"props": { "gap": "md" },
"children": ["nameInput", "emailInput", "messageInput"]
},
"nameInput": {
"type": "Input",
"props": {
"label": "Name",
"value": { "$bindState": "/form/name" },
"checks": [{ "type": "required", "message": "Name is required" }]
}
},
"emailInput": {
"type": "Input",
"props": {
"label": "Email",
"type": "email",
"value": { "$bindState": "/form/email" },
"checks": [
{ "type": "required", "message": "Email is required" },
{ "type": "email", "message": "Enter a valid email" }
],
"validateOn": "blur"
}
},
"messageInput": {
"type": "Textarea",
"props": {
"label": "Message",
"rows": 4,
"value": { "$bindState": "/form/message" }
}
},
"submitBtn": {
"type": "Button",
"props": { "label": "Send Message" },
"on": {
"press": {
"action": "showToast",
"params": { "title": "Message sent!", "variant": "success" }
}
}
}
}
}#Setup API
These are the programmatic APIs for setting up the renderer. Most projects only need the pre-built registry.
#Pre-built registry (recommended)
import { registry, toastManager } from "@/lib/json-render";
import type { Spec } from "@json-render/react";#System prompt generation
Generate a system prompt from the component catalog for AI models:
import { catalog } from "@/lib/json-render";
const systemPrompt = catalog.prompt({
customRules: [
"Always wrap the root in a Box with padding='lg'",
"Start with a heading (Text variant='h3')",
],
});For server-side API routes (avoids importing React):
import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/react/schema";
import {
dinachiComponentDefinitions,
dinachiActionDefinitions,
} from "@/lib/json-render/catalog";
const catalog = defineCatalog(schema, {
components: dinachiComponentDefinitions,
actions: dinachiActionDefinitions,
});
const systemPrompt = catalog.prompt({ customRules: [...] });#Composing with an existing json-render project
If you already have your own json-render catalog, spread Dinachi's definitions in:
import { defineCatalog } from "@json-render/core";
import { schema, defineRegistry } from "@json-render/react";
import {
dinachiComponentDefinitions,
dinachiActionDefinitions,
dinachiComponents,
dinachiActionHandlers,
} from "@/lib/json-render";
const catalog = defineCatalog(schema, {
components: { ...yourDefs, ...dinachiComponentDefinitions },
actions: { ...yourActions, ...dinachiActionDefinitions },
});
const { registry } = defineRegistry(catalog, {
components: { ...yourComponents, ...dinachiComponents },
actions: { ...yourHandlers, ...dinachiActionHandlers },
});#Overriding a component
import { defineRegistry } from "@json-render/react";
import { catalog, dinachiComponents, dinachiActionHandlers } from "@/lib/json-render";
const { registry } = defineRegistry(catalog, {
components: {
...dinachiComponents,
Button: ({ props, emit }) => (
<button className="custom-btn" onClick={() => emit?.("press")}>
{props.label}
</button>
),
},
actions: dinachiActionHandlers,
});#Try It Live
See the Playground to generate DinachiUI interfaces from natural language.