json-render-generative-ui
Generative UI framework that renders AI-generated JSON specs into type-safe UI components across React, Vue, Svelte, Solid, React Native, video, PDF, and email.
Install
npx skills add https://github.com/reason-machines/trending-skills --skill json-render-generative-uiSKILL.md
json-render Generative UI Framework
Skill by ara.so — Daily 2026 Skills collection.
json-render is a Generative UI framework that lets AI generate dynamic interfaces from natural language prompts, constrained to a predefined component catalog. AI outputs JSON; json-render renders it safely and predictably across any platform.
Installation
# React (core)
npm install @json-render/core @json-render/react
# React + shadcn/ui (36 pre-built components)
npm install @json-render/shadcn
# React Native
npm install @json-render/core @json-render/react-native
# Vue
npm install @json-render/core @json-render/vue
# Svelte
npm install @json-render/core @json-render/svelte
# SolidJS
npm install @json-render/core @json-render/solid
# Video (Remotion)
npm install @json-render/core @json-render/remotion
# PDF
npm install @json-render/core @json-render/react-pdf
# Email
npm install @json-render/core @json-render/react-email @react-email/components @react-email/render
# 3D (React Three Fiber)
npm install @json-render/core @json-render/react-three-fiber @react-three/fiber @react-three/drei three
# OG Images / SVG / PNG
npm install @json-render/core @json-render/image
# State management adapters
npm install @json-render/zustand # or redux, jotai, xstate
# MCP integration (Claude, ChatGPT, Cursor)
npm install @json-render/mcp
# YAML wire format
npm install @json-render/yaml
Core Concepts
| Concept | Description |
|---|---|
| Catalog | Defines allowed components and actions (the guardrails for AI) |
| Spec | AI-generated JSON describing which components to render and with what props |
| Registry | Maps catalog component names to actual render implementations |
| Renderer | Platform-specific component that takes a spec + registry and renders UI |
| Actions | Named events AI can trigger (e.g. export_report, refresh_data) |
Spec Format
The flat spec format uses a root key + elements map:
const spec = {
root: "card-1",
elements: {
"card-1": {
type: "Card",
props: { title: "Dashboard" },
children: ["metric-1", "metric-2", "button-1"],
},
"metric-1": {
type: "Metric",
props: { label: "Revenue", value: "124000", format: "currency" },
children: [],
},
"metric-2": {
type: "Metric",
props: { label: "Growth", value: "0.18", format: "percent" },
children: [],
},
"button-1": {
type: "Button",
props: { label: "Export Report", action: "export_report" },
children: [],
},
},
};
Step 1: Define a 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 with a title",
},
Metric: {
props: z.object({
label: z.string(),
value: z.string(),
format: z.enum(["currency", "percent", "number"]).nullable(),
}),
description: "Displays a single metric value with optional formatting",
},
Button: {
props: z.object({
label: z.string(),
action: z.string(),
}),
description: "Clickable button that triggers an action",
},
Stack: {
props: z.object({
direction: z.enum(["row", "column"]).default("column"),
gap: z.number().optional(),
}),
description: "Layout container that stacks children",
},
},
actions: {
export_report: { description: "Export the current dashboard to PDF" },
refresh_data: { description: "Refresh all metric data" },
navigate: {
description: "Navigate to a page",
payload: z.object({ path: z.string() }),
},
},
});
Step 2: Define a Registry (React)
import { defineRegistry, Renderer } from "@json-render/react";
function format(value: string, fmt: string | null): string {
if (fmt === "currency") return `$${Number(value).toLocaleString()}`;
if (fmt === "percent") return `${(Number(value) * 100).toFixed(1)}%`;
return value;
}
const { registry } = defineRegistry(catalog, {
components: {
Card: ({ props, children }) => (
<div className="rounded-lg border p-4 shadow-sm">
<h3 className="text-lg font-semibold mb-3">{props.title}</h3>
{children}
</div>
),
Metric: ({ props }) => (
<div className="flex flex-col">
<span className="text-sm text-gray-500">{props.label}</span>
<span className="text-2xl font-bold">
{format(props.value, props.format)}
</span>
</div>
),
Button: ({ props, emit }) => (
<button
className="px-4 py-2 bg-blue-600 text-white rounded"
onClick={() => emit("press")}
>
{props.label}
</button>
),
Stack: ({ props, children }) => (
<div
style={{
display: "flex",
flexDirection: props.direction ?? "column",
gap: props.gap ?? 8,
}}
>
{children}
</div>
),
},
});
Step 3: Render the Spec
import { Renderer } from "@json-render/react";
function Dashboard({ spec, onAction }) {
return (
<Renderer
spec={spec}
registry={registry}
onAction={(action, payload) => {
console.log("Action triggered:", action, payload);
onAction?.(action, payload);
}}
/>
);
}
Generating Specs with AI (Vercel AI SDK)
import { generateObject } from "ai";
import { openai } from "@ai-sdk/openai";
import { getCatalogSchema, getCatalogPrompt } from "@json-render/core";
async function generateDashboard(userPrompt: string) {
const { object: spec } = await generateObject({
model: openai("gpt-4o"),
schema: getCatalogSchema(catalog),
system: getCatalogPrompt(catalog),
prompt: userPrompt,
});
return spec;
}
// Usage
const spec = await generateDashboard(
"Create a sales dashboard showing revenue, conversion rate, and an export button"
);
Streaming Specs
import { streamObject } from "ai";
import { openai } from "@ai-sdk/openai";
import { getCatalogSchema, getCatalogPrompt, parseSpecStream } from "@json-render/core";
import { Renderer } from "@json-render/react";
import { useState, useEffect } from "react";
function StreamingDashboard({ prompt }: { prompt: string }) {
const [spec, setSpec] = useState(null);
useEffect(() => {
async function stream() {
const { partialObjectStream } = await streamObject({
model: openai("gpt-4o"),
schema: getCatalogSchema(catalog),
system: getCatalogPrompt(catalog),
prompt,
});
for await (const partial of partialObjectStream) {
setSpec(partial); // Renderer handles partial specs gracefully
}
}
stream();
}, [prompt]);
if (!spec) return <div>Generating UI...</div>;
return <Renderer spec={spec} registry={registry} />;
}
Using Pre-built shadcn/ui Components
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 any of the 36 available shadcn components
const catalog = defineCatalog(schema, {
components: {
Card: shadcnComponentDefinitions.Card,
Stack: shadcnComponentDefinitions.Stack,
Heading: shadcnComponentDefinitions.Heading,
Text: shadcnComponentDefinitions.Text,
Button: shadcnComponentDefinitions.Button,
Badge: shadcnComponentDefinitions.Badge,
Table: shadcnComponentDefinitions.Table,
Chart: shadcnComponentDefinitions.Chart,
Input: shadcnComponentDefinitions.Input,
Select: shadcnComponentDefinitions.Select,
},
actions: {
submit: { description: "Submit a form" },
export: { description: "Export data" },
},
});
const { registry } = defineRegistry(catalog, {
components: {
Card: shadcnComponents.Card,
Stack: shadcnComponents.Stack,
Heading: shadcnComponents.Heading,
Text: shadcnComponents.Text,
Button: shadcnComponents.Button,
Badge: shadcnComponents.Badge,
Table: shadcnComponents.Table,
Chart: shadcnComponents.Chart,
Input: shadcnComponents.Input,
Select: shadcnComponents.Select,
},
});
function AIPage({ spec }) {
return <Renderer spec={spec} registry={registry} />;
}
Vue Renderer
import { h, defineComponent } from "vue";
import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/vue/schema";
import { defineRegistry, Renderer } from "@json-render/vue";
import { z } from "zod";
const catalog = defineCatalog(schema, {
components: {
Card: {
props: z.object({ title: z.string() }),
description: "Card container",
},
Button: {
props: z.object({ label: z.string() }),
description: "Button",
},
},
actions: {
click: { description: "Button clicked" },
},
});
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("click") }, props.label),
},
});
// In your Vue SFC:
// <template>
// <Renderer :spec="spec" :registry="registry" />
// </template>
React Native Renderer
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 mobile components out of the box
const catalog = defineCatalog(schema, {
components: { ...standardComponentDefinitions },
actions: standardActionDefinitions,
});
const { registry } = defineRegistry(catalog, {
components: {}, // use all standard implementations
});
export function AIScreen({ spec }) {
return <Renderer spec={spec} registry={registry} />;
}
PDF Generation
import { renderToBuffer } from "@json-render/react-pdf";
const invoiceSpec = {
root: "doc",
elements: {
doc: {
type: "Document",
props: { title: "Invoice #1234" },
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: "Amount", width: "40%", align: "right" },
],
rows: [
["Widget A", "$10.00"],
["Widget B", "$25.00"],
["Total", "$35.00"],
],
},
children: [],
},
},
};
// Returns a Buffer you can send as a response
const buffer = await renderToBuffer(invoiceSpec);
// In a Next.js route handler:
export async function GET() {
const buffer = await renderToBuffer(invoiceSpec);
return new Response(buffer, {
headers: { "Content-Type": "application/pdf" },
});
}
Email Generation
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 emailSpec = {
root: "html-1",
elements: {
"html-1": {
type: "Html",
props: { lang: "en" },
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" } },
children: ["heading-1", "text-1", "button-1"],
},
"heading-1": {
type: "Heading",
props: { text: "Welcome aboard!" },
children: [],
},
"text-1": {
type: "Text",
props: { text: "Thanks for signing up. Click below to get started." },
children: [],
},
"button-1": {
type: "Button",
props: { text: "Get Started", href: "https://example.com" },
children: [],
},
},
};
const html = await renderToHtml(emailSpec);
MCP Integration (Claude, ChatGPT, Cursor)
import { createMCPServer } from "@json-render/mcp";
const server = createMCPServer({
catalog,
name: "my-ui-server",
version: "1.0.0",
});
server.start();
State Management Integration
import { create } from "zustand";
import { createZustandAdapter } from "@json-render/zustand";
const useStore = create((set) => ({
data: {},
setData: (data) => set({ data }),
}));
const stateStore = createZustandAdapter(useStore);
// Pass to Renderer for action handling with state
<Renderer spec={spec} registry={registry} stateStore={stateStore} />;
YAML Wire Format
import { parseYAML, toYAML } from "@json-render/yaml";
// AI can output YAML instead of JSON (often more token-efficient)
const yamlSpec = `
root: card-1
elements:
card-1:
type: Card
props:
title: Hello World
children: [button-1]
button-1:
type: Button
props:
label: Click Me
children: []
`;
const spec = parseYAML(yamlSpec);
Full Next.js App Router Example
// app/dashboard/page.tsx
import { generateObject } from "ai";
import { openai } from "@ai-sdk/openai";
import { getCatalogSchema, getCatalogPrompt } from "@json-render/core";
import { DashboardRenderer } from "./DashboardRenderer";
import { catalog } from "@/lib/catalog";
export default async function DashboardPage({
searchParams,
}: {
searchParams: { q?: string };
}) {
const prompt = searchParams.q ?? "Show me a sales overview dashboard";
const { object: spec } = await generateObject({
model: openai("gpt-4o"),
schema: getCatalogSchema(catalog),
system: getCatalogPrompt(catalog),
prompt,
});
return <DashboardRenderer spec={spec} />;
}
// app/dashboard/DashboardRenderer.tsx
"use client";
import { Renderer } from "@json-render/react";
import { registry } from "@/lib/registry";
import { useRouter } from "next/navigation";
export function DashboardRenderer({ spec }) {
const router = useRouter();
return (
<Renderer
spec={spec}
registry={registry}
onAction={(action, payload) => {
switch (action) {
case "navigate":
router.push(payload.path);
break;
case "export_report":
window.open("/api/export", "_blank");
break;
case "refresh_data":
router.refresh();
break;
}
}}
/>
);
}
Common Patterns
Conditional Component Availability
// Restrict catalog based on user role
function getCatalogForRole(role: "admin" | "viewer") {
const base = { Card, Stack, Heading, Text, Metric };
const adminOnly = role === "admin" ? { Button, Form, Table } : {};
const adminActions = role === "admin"
? { export: { description: "Export data" } }
: {};
return defineCatalog(schema, {
components: { ...base, ...adminOnly },
actions: adminOnly ? adminActions : {},
});
}
Dynamic Props with Runtime Data
// Components can fetch their own data
const { registry } = defineRegistry(catalog, {
components: {
LiveMetric: ({ props }) => {
const { data } = useSWR(`/api/metrics/${props.metricId}`);
return (
<div>
<span>{props.label}</span>
<span>{data?.value ?? "..."}</span>
</div>
);
},
},
});
Type-Safe Action Handling
import { type ActionHandler } from "@json-render/core";
const handleAction: ActionHandler<typeof catalog> = (action, payload) => {
// action and payload are fully typed based on your catalog definition
if (action === "navigate") {
router.push(payload.path); // payload.path is typed as string
}
};
Troubleshooting
| Problem | Cause | Fix |
|---|---|---|
| AI generates unknown component type | Component not in catalog | Add component to defineCatalog or update AI prompt |
| Props validation error | AI hallucinated a prop | Tighten Zod schema, add .strict() or .describe() hints |
| Renderer shows nothing | root key doesn't match an elements key |
Check spec structure; root must reference a valid element ID |
| Partial spec renders incorrectly | Streaming not handled | Use parseSpecStream utility or check for null elements before render |
| Actions not firing | onAction not passed to Renderer |
Pass onAction prop to <Renderer> |
| shadcn components unstyled | Missing Tailwind config | Ensure @json-render/shadcn paths are in tailwind.config.js content array |
| TypeScript errors in registry | Catalog/registry mismatch | Ensure defineRegistry(catalog, ...) uses the same catalog instance |
Environment Variables
# For AI generation (use your preferred provider)
OPENAI_API_KEY=your_key_here
ANTHROPIC_API_KEY=your_key_here
# For MCP server
MCP_SERVER_PORT=3001
Key API Reference
// Core
defineCatalog(schema, { components, actions }) // Define guardrails
getCatalogSchema(catalog) // Get Zod schema for AI
getCatalogPrompt(catalog) // Get system prompt for AI
// React
defineRegistry(catalog, { components }) // Create typed registry
<Renderer spec={spec} registry={registry} onAction={fn} />
// Core utilities
parseSpecStream(stream) // Parse streaming partial specs
toYAML(spec) // Convert spec to YAML
parseYAML(yaml) // Parse YAML spec to JSON
// PDF
renderToBuffer(spec) // → Buffer
renderToStream(spec) // → ReadableStream
// Email
renderToHtml(spec) // → HTML string
renderToText(spec) // → plain text string
Related skills
vercel-react-best-practicesvercel-labs751KReact and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.vercel-composition-patternsvercel-labs361KReact composition patterns that scale. Use when refactoring components with boolean prop proliferation, building flexible component libraries, or designing reusable APIs. Triggers on tasks involving compound components, render props, context providers, or component architecture. Includes React 19 API changes.vercel-react-view-transitionsvercel-labs135KGuide for implementing smooth, native-feeling animations using React's View Transition API (`<ViewTransition>` component, `addTransitionType`, and CSS view transition pseudo-elements). Use this skill whenever the user wants to add page transitions, animate route changes, create shared element animations, animate enter/exit of components, animate list reorder, implement directional (forward/back) navigation animations, or integrate view transitions in Next.js. Also use when the user mentions viewpick-ui-libraryemilkowalski124KPick the right library for a given frontend task from a curated, opinionated list — numbers, OTP inputs, charts, command menus, virtualization, drag and drop, toasts, state, styling, and more. Only runs when explicitly invoked; it does not trigger on its own.