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.

INSTALLATION
npx skills add https://github.com/aradotso/trending-skills --skill json-render-generative-ui
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

$27

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

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card