json-render
用 catalog + Zod schema 给 LLM 划好边界,让模型只输出你预设组件与 action 组成的 JSON,再由渲染器还原成真实界面的 Generative UI 框架,支持流式渐进渲染和 $state/$cond 等动态表达式。亮点是同一份 catalog 可落到 React、Vue、Svelte、Solid、React Native、Remotion 视频、PDF、邮件、终端 Ink 甚至 Three.js 场景,自带上百个 shadcn 组件定义和 devtools 面板,还提供 MCP Apps 集成方便接 Claude、Cursor 等客户端,适合做动态仪表盘、报表和个性化界面。Apache-2.0 协议。
README
json-render
生成式 UI(Generative UI)框架。
从 prompt 生成动态、个性化的 UI,同时不牺牲可靠性。预定义组件与 action,确保输出安全、可预测。
# for React
npm install @json-render/core @json-render/react
# for React with pre-built shadcn/ui components
npm install @json-render/shadcn
# or for React Native
npm install @json-render/core @json-render/react-native
# or for video
npm install @json-render/core @json-render/remotion
# or for PDF documents
npm install @json-render/core @json-render/react-pdf
# or for HTML email
npm install @json-render/core @json-render/react-email @react-email/components @react-email/render
# or for Vue
npm install @json-render/core @json-render/vue
# or for Svelte
npm install @json-render/core @json-render/svelte
# or for SolidJS
npm install @json-render/core @json-render/solid
# or for terminal UIs
npm install @json-render/core @json-render/ink ink react
# or for full Next.js apps (routes, layouts, SSR, metadata)
npm install @json-render/core @json-render/react @json-render/next
# or for 3D scenes (and gaussian splatting via the GaussianSplat component)
npm install @json-render/core @json-render/react-three-fiber @react-three/fiber @react-three/drei three
为什么选择 json-render?
json-render 是一个 生成式 UI(Generative UI) 框架:AI 从自然语言 prompt 生成界面,并限定在你定义的组件范围内。你设定护栏,AI 在其中生成:
- 有护栏约束(Guardrailed) - AI 只能使用你 catalog 中的组件
- 可预测(Predictable) - JSON 输出每次都符合你的 schema
- 快速(Fast) - 随着模型响应流式、渐进地渲染
- 跨平台(Cross-Platform) - 同一份 catalog 可用于 React、Vue、Svelte、Solid(Web)以及 React Native(移动端)
- 开箱即用(Batteries Included) - 36 个预置 shadcn/ui 组件,拿来即用
快速开始
1. 定义你的 Catalog
import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/react/schema";
import { z } from "zod";
const catalog = defineCatalog(schema, {
components: {
Card: {
props: z.object({ title: z.string() }),
description: "A card container",
},
Metric: {
props: z.object({
label: z.string(),
value: z.string(),
format: z.enum(["currency", "percent", "number"]).nullable(),
}),
description: "Display a metric value",
},
Button: {
props: z.object({
label: z.string(),
action: z.string(),
}),
description: "Clickable button",
},
},
actions: {
export_report: { description: "Export dashboard to PDF" },
refresh_data: { description: "Refresh all metrics" },
},
});
2. 定义你的组件
import { defineRegistry, Renderer } from "@json-render/react";
const { registry } = defineRegistry(catalog, {
components: {
Card: ({ props, children }) => (
<div className="card">
<h3>{props.title}</h3>
{children}
</div>
),
Metric: ({ props }) => (
<div className="metric">
<span>{props.label}</span>
<span>{format(props.value, props.format)}</span>
</div>
),
Button: ({ props, emit }) => (
<button onClick={() => emit("press")}>{props.label}</button>
),
},
});
3. 渲染 AI 生成的 spec
function Dashboard({ spec }) {
return <Renderer spec={spec} registry={registry} />;
}
就这么简单。 AI 生成 JSON,你安全地渲染它。
包(Packages)
| 包 | 描述 |
|---|---|
@json-render/core |
Schema、catalog、AI prompt、动态 props、SpecStream 工具 |
@json-render/react |
React renderer、context、hooks |
@json-render/vue |
Vue 3 renderer、composables、providers |
@json-render/svelte |
基于 runes 响应式的 Svelte 5 renderer |
@json-render/solid |
具有细粒度响应式 context 的 SolidJS renderer |
@json-render/shadcn |
36 个预置 shadcn/ui 组件(Radix UI + Tailwind CSS) |
@json-render/shadcn-svelte |
36 个预置 shadcn-svelte 组件(Svelte 5 + Tailwind CSS) |
@json-render/react-three-fiber |
用于 3D 场景的 React Three Fiber renderer(20 个内置组件,含 GaussianSplat) |
@json-render/react-native |
带标准移动端组件的 React Native renderer |
@json-render/next |
Next.js renderer —— JSON 可变成带路由、布局、SSR 的完整 app |
@json-render/tanstack-start |
TanStack Start renderer —— 带路由、布局、SSR 与 head metadata 的完整 app |
@json-render/remotion |
Remotion 视频 renderer、timeline schema |
@json-render/react-pdf |
用于从 spec 生成 PDF 文档的 React PDF renderer |
@json-render/react-email |
用于从 spec 生成 HTML/纯文本邮件的 React Email renderer |
@json-render/ink |
带内置组件的 Ink 终端 renderer,用于交互式 TUI |
@json-render/image |
通过 Satori 输出 SVG/PNG(OG 图、社交卡片)的 image renderer |
@json-render/directives |
预置自定义 directive —— $format、$math、$concat、$count、$truncate、$pluralize、$join、$t(i18n) |
@json-render/codegen |
从 json-render UI 树生成代码的工具 |
@json-render/devtools |
框架无关的 devtools 核心 —— 面板 UI、事件存储、picker、stream tap |
@json-render/devtools-react |
@json-render/devtools 的 React 适配器(可直接使用 <JsonRenderDevtools />) |
@json-render/devtools-vue |
@json-render/devtools 的 Vue 适配器 |
@json-render/devtools-svelte |
@json-render/devtools 的 Svelte 适配器 |
@json-render/devtools-solid |
@json-render/devtools 的 SolidJS 适配器 |
@json-render/redux |
StateStore 的 Redux / Redux Toolkit 适配器 |
@json-render/zustand |
StateStore 的 Zustand 适配器 |
@json-render/jotai |
StateStore 的 Jotai 适配器 |
@json-render/xstate |
StateStore 的 XState Store(atom)适配器 |
@json-render/mcp |
面向 Claude、ChatGPT、Cursor、VS Code 的 MCP Apps 集成 |
@json-render/yaml |
带流式 parser、编辑模式、AI SDK 转换的 YAML 传输格式 |
Renderer
React(UI)
import { defineRegistry, Renderer } from "@json-render/react";
import { schema } from "@json-render/react/schema";
// Flat spec format (root key + elements map)
const spec = {
root: "card-1",
elements: {
"card-1": {
type: "Card",
props: { title: "Hello" },
children: ["button-1"],
},
"button-1": {
type: "Button",
props: { label: "Click me" },
children: [],
},
},
};
// defineRegistry creates a type-safe component registry
const { registry } = defineRegistry(catalog, { components });
<Renderer spec={spec} registry={registry} />;
Vue(UI)
import { h } from "vue";
import { defineRegistry, Renderer } from "@json-render/vue";
import { schema } from "@json-render/vue/schema";
const { registry } = defineRegistry(catalog, {
components: {
Card: ({ props, children }) =>
h("div", { class: "card" }, [h("h3", null, props.title), children]),
Button: ({ props, emit }) =>
h("button", { onClick: () => emit("press") }, props.label),
},
});
// In your Vue component template:
// <Renderer :spec="spec" :registry="registry" />
Svelte(UI)
import { defineRegistry, Renderer } from "@json-render/svelte";
import { schema } from "@json-render/svelte/schema";
const { registry } = defineRegistry(catalog, {
components: {
Card: ({ props, children }) => /* Svelte 5 snippet */,
Button: ({ props, emit }) => /* Svelte 5 snippet */,
},
});
// In your Svelte component:
// <Renderer spec={spec} registry={registry} />
Solid(UI)
import { defineRegistry, Renderer } from "@json-render/solid";
import { schema } from "@json-render/solid/schema";
const { registry } = defineRegistry(catalog, {
components: {
Card: (renderProps) => <div>{renderProps.children}</div>,
Button: (renderProps) => (
<button onClick={() => renderProps.emit("press")}>
{renderProps.element.props.label as string}
</button>
),
},
});
<Renderer spec={spec} registry={registry} />;
shadcn/ui(Web)
import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/react/schema";
import { defineRegistry, Renderer } from "@json-render/react";
import { shadcnComponentDefinitions } from "@json-render/shadcn/catalog";
import { shadcnComponents } from "@json-render/shadcn";
// Pick components from the 36 standard definitions
const catalog = defineCatalog(schema, {
components: {
Card: shadcnComponentDefinitions.Card,
Stack: shadcnComponentDefinitions.Stack,
Heading: shadcnComponentDefinitions.Heading,
Button: shadcnComponentDefinitions.Button,
},
actions: {},
});
// Use matching implementations
const { registry } = defineRegistry(catalog, {
components: {
Card: shadcnComponents.Card,
Stack: shadcnComponents.Stack,
Heading: shadcnComponents.Heading,
Button: shadcnComponents.Button,
},
});
<Renderer spec={spec} registry={registry} />;
React Native(移动端)
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 } from "@json-render/react-native";
// 25+ standard components included
const catalog = defineCatalog(schema, {
components: { ...standardComponentDefinitions },
actions: standardActionDefinitions,
});
const { registry } = defineRegistry(catalog, { components: {} });
<Renderer spec={spec} registry={registry} />;
Remotion(视频)
import { Player } from "@remotion/player";
import {
Renderer,
schema,
standardComponentDefinitions,
} from "@json-render/remotion";
// Timeline spec format
const spec = {
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: [] },
};
<Player
component={Renderer}
inputProps={{ spec }}
durationInFrames={spec.composition.durationInFrames}
fps={spec.composition.fps}
compositionWidth={spec.composition.width}
compositionHeight={spec.composition.height}
/>;
React PDF(文档)
import { renderToBuffer } from "@json-render/react-pdf";
const spec = {
root: "doc",
elements: {
doc: {
type: "Document",
props: { title: "Invoice" },
children: ["page-1"],
},
"page-1": {
type: "Page",
props: { size: "A4" },
children: ["heading-1", "table-1"],
},
"heading-1": {
type: "Heading",
props: { text: "Invoice #1234", level: "h1" },
children: [],
},
"table-1": {
type: "Table",
props: {
columns: [
{ header: "Item", width: "60%" },
{ header: "Price", width: "40%", align: "right" },
],
rows: [
["Widget A", "$10.00"],
["Widget B", "$25.00"],
],
},
children: [],
},
},
};
// Render to buffer, stream, or file
const buffer = await renderToBuffer(spec);
React Email(邮件)
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);
Image(SVG/PNG)
import { renderToPng } from "@json-render/image/render";
const 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: [],
},
},
};
// Render to PNG (requires @resvg/resvg-js)
const png = await renderToPng(spec, { fonts });
// Or render to SVG string
import { renderToSvg } from "@json-render/image/render";
const svg = await renderToSvg(spec, { fonts });
Three.js(3D)
import { defineCatalog } from "@json-render/core";
import { schema, defineRegistry } from "@json-render/react";
import {
threeComponentDefinitions,
threeComponents,
ThreeCanvas,
} from "@json-render/react-three-fiber";
const catalog = defineCatalog(schema, {
components: {
Box: threeComponentDefinitions.Box,
Sphere: threeComponentDefinitions.Sphere,
AmbientLight: threeComponentDefinitions.AmbientLight,
DirectionalLight: threeComponentDefinitions.DirectionalLight,
GaussianSplat: threeComponentDefinitions.GaussianSplat,
OrbitControls: threeComponentDefinitions.OrbitControls,
},
actions: {},
});
const { registry } = defineRegistry(catalog, {
components: {
Box: threeComponents.Box,
Sphere: threeComponents.Sphere,
AmbientLight: threeComponents.AmbientLight,
DirectionalLight: threeComponents.DirectionalLight,
GaussianSplat: threeComponents.GaussianSplat,
OrbitControls: threeComponents.OrbitControls,
},
});
<ThreeCanvas
spec={spec}
registry={registry}
shadows
camera={{ position: [5, 5, 5], fov: 50 }}
style={{ width: "100%", height: "100vh" }}
/>;
Next.js(完整 App)
import type { NextAppSpec } from "@json-render/next";
import { createNextApp } from "@json-render/next/server";
import { NextAppProvider } from "@json-render/next";
const spec: NextAppSpec = {
metadata: { title: { default: "My App", template: "%s | My App" } },
layouts: {
main: {
root: "shell",
elements: {
shell: { type: "Container", props: {}, children: ["nav", "slot"] },
nav: { type: "NavBar", props: {}, children: [] },
slot: { type: "Slot", props: {}, children: [] },
},
},
},
routes: {
"/": {
layout: "main",
metadata: { title: "Home" },
page: {
root: "hero",
elements: {
hero: { type: "Card", props: { title: "Welcome" }, children: [] },
},
},
},
},
};
// Server: creates Page, generateMetadata, generateStaticParams
const app = createNextApp({ spec });
// Client: wrap your layout with NextAppProvider
// <NextAppProvider registry={registry} handlers={handlers}>
// {children}
// </NextAppProvider>
TanStack Start(完整 App)
import { createFileRoute, notFound } from "@tanstack/react-router";
import {
PageRenderer,
StartErrorBoundary,
StartLoading,
StartNotFound,
type StartAppSpec,
} from "@json-render/tanstack-start";
import { createStartApp } from "@json-render/tanstack-start/server";
const spec: StartAppSpec = {
metadata: { title: { default: "My App", template: "%s | My App" } },
routes: {
"/": {
metadata: { title: "Home" },
page: {
root: "hero",
elements: {
hero: { type: "Card", props: { title: "Welcome" }, children: [] },
},
},
},
},
};
const { getPageData, getHead } = createStartApp({ spec });
export const Route = createFileRoute("/$")({
loader: async ({ location }) => {
const data = await getPageData({ pathname: location.pathname });
if (!data) throw notFound();
return data;
},
head: ({ match }) => getHead({ pathname: match.pathname }),
component: () => <PageRenderer {...Route.useLoaderData()} />,
pendingComponent: StartLoading,
errorComponent: StartErrorBoundary,
notFoundComponent: StartNotFound,
});
用 <StartAppProvider spec={spec}> 包裹根路由的 outlet,这样路由的 fallback 组件就能解析出当前路由。通过它的 functions prop 传入具名的 $computed 实现。
shadcn-svelte(Svelte)
import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/svelte/schema";
import { defineRegistry, Renderer } from "@json-render/svelte";
import { shadcnComponentDefinitions } from "@json-render/shadcn-svelte/catalog";
import { shadcnComponents } from "@json-render/shadcn-svelte";
const catalog = defineCatalog(schema, {
components: {
Card: shadcnComponentDefinitions.Card,
Stack: shadcnComponentDefinitions.Stack,
Heading: shadcnComponentDefinitions.Heading,
Button: shadcnComponentDefinitions.Button,
},
actions: {},
});
const { registry } = defineRegistry(catalog, {
components: {
Card: shadcnComponents.Card,
Stack: shadcnComponents.Stack,
Heading: shadcnComponents.Heading,
Button: shadcnComponents.Button,
},
});
// In your Svelte component:
// <Renderer spec={spec} registry={registry} />
Devtools
面向任何 json-render app 的即插即用检查面板。包含 spec 树、状态编辑器、action 日志、stream 日志、catalog 浏览器、DOM picker。
// React
import { JsonRenderDevtools } from "@json-render/devtools-react";
<JSONUIProvider registry={registry} handlers={handlers}>
<Renderer spec={spec} registry={registry} />
<JsonRenderDevtools spec={spec} catalog={catalog} messages={messages} />
</JSONUIProvider>;
右下角会出现一个浮动开关。快捷键:Ctrl/Cmd + Shift + J。生产环境会被 tree-shake 成 null。
可用于 React、Vue、Svelte 和 Solid —— 只需把 @json-render/devtools-react 换成与你 renderer 匹配的适配器。
Ink(终端)
import { defineCatalog } from "@json-render/core";
import {
schema,
standardComponentDefinitions,
standardActionDefinitions,
defineRegistry,
Renderer,
JSONUIProvider,
} from "@json-render/ink";
const catalog = defineCatalog(schema, {
components: { ...standardComponentDefinitions },
actions: standardActionDefinitions,
});
const { registry } = defineRegistry(catalog, { components: {} });
const spec = {
root: "card-1",
elements: {
"card-1": {
type: "Card",
props: { title: "Status" },
children: ["status-1"],
},
"status-1": {
type: "StatusLine",
props: { label: "Build", status: "success" },
children: [],
},
},
};
<JSONUIProvider initialState={{}}>
<Renderer spec={spec} registry={registry} />
</JSONUIProvider>;
特性
流式处理(SpecStream)
流式、渐进地处理 AI 响应:
import { createSpecStreamCompiler } from "@json-render/core";
const compiler = createSpecStreamCompiler<MySpec>();
// Process chunks as they arrive
const { result, newPatches } = compiler.push(chunk);
setSpec(result); // Update UI with partial result
// Get final result
const finalSpec = compiler.getResult();
AI Prompt 生成
从你的 catalog 生成 system prompt:
const systemPrompt = catalog.prompt();
// Includes component descriptions, props schemas, available actions
条件可见性
{
"type": "Alert",
"props": { "message": "Error occurred" },
"visible": [
{ "$state": "/form/hasError" },
{ "$state": "/form/errorDismissed", "not": true }
]
}
动态 Props
任何 prop 值都可以通过表达式由数据驱动:
{
"type": "Icon",
"props": {
"name": {
"$cond": { "$state": "/activeTab", "eq": "home" },
"$then": "home",
"$else": "home-outline"
},
"color": {
"$cond": { "$state": "/activeTab", "eq": "home" },
"$then": "#007AFF",
"$else": "#8E8E93"
}
}
}
表达式形式:
{ "$state": "/state/key" }- 从 state model 中读取一个值{ "$cond": <condition>, "$then": <value>, "$else": <value> }- 评估条件并选择一个分支{ "$template": "Hello, ${/user/name}!" }- 将 state 值插值到字符串中{ "$computed": "fn", "args": { ... } }- 用已解析的 args 调用注册的函数
Actions
组件可以触发 action,包括内置的 setState action:
{
"type": "Pressable",
"props": {
"action": "setState",
"actionParams": { "statePath": "/activeTab", "value": "home" }
},
"children": ["home-icon"]
}
setState action 直接更新 state model,进而重新评估可见性条件和动态 prop 表达式。
State Watchers
通过触发 action 来响应 state 变化:
{
"type": "Select",
"props": {
"value": { "$bindState": "/form/country" },
"options": ["US", "Canada", "UK"]
},
"watch": {
"/form/country": {
"action": "loadCities",
"params": { "country": { "$state": "/form/country" } }
}
}
}
watch 是元素上的顶层字段(与 type/props/children 同级)。Watcher 在被监听的值发生变化时触发,而不是在初始渲染时触发。
演示
git clone https://github.com/vercel-labs/json-render
cd json-render
pnpm install
pnpm dev
- http://json-render.localhost:1355 - 文档与 Playground
- http://dashboard-demo.json-render.localhost:1355 - 示例 Dashboard
- http://react-email-demo.json-render.localhost:1355 - React Email 示例
- http://remotion-demo.json-render.localhost:1355 - Remotion 视频示例
- Chat 示例:在
examples/chat中运行pnpm dev - 实验性 Jev 组合:使用 core 中的
experimental_composeSpec和experimental_createEvaluator搭配你自己的 catalog,或在/playground中选择 Jev (Experimental)。尚未发布;源码构建说明见该指南。 - Svelte 示例:在
examples/svelte或examples/svelte-chat中运行pnpm dev - Vue 示例:在
examples/vue中运行pnpm dev - Vite Renderers(React + Vue + Svelte + Solid):在
examples/vite-renderers中运行pnpm dev - React Native 示例:在
examples/react-native中运行npx expo start - Gaussian Splatting(R3F):在
examples/react-three-fiber-gsplat中运行pnpm dev - Gaussian Splatting(实验性独立 gsplat.js 示例):在
examples/gsplat中运行pnpm dev
工作原理
flowchart LR
A[User Prompt] --> B[AI + Catalog]
B --> C[JSON Spec]
C --> D[Renderer]
B -.- E([guardrailed])
C -.- F([predictable])
D -.- G([streamed])
- 定义护栏 - 规定 AI 可以使用哪些组件、action 和数据绑定
- 写 Prompt - 用自然语言描述你想要的东西
- AI 生成 JSON - 输出始终可预测,限定在你的 catalog 内
- 快速渲染 - 随着模型响应流式、渐进地渲染
License
Apache-2.0