vercel-labs/json-render

▲ 585 stars today★ 16,967⑂ 905

The Generative UI framework

About vercel-labs/json-render

vercel-labs/json-render is an open-source project on GitHub, mainly written in TypeScript. The Generative UI framework It currently holds 16,967 stars and 905 forks with 103 open issues, and was last pushed on 2026-09-18 (repository created 2026-01-14).

Project Overview

AI Homed tracks it on the Today's Trending board, currently at rank #4 with 585 new stars today.

GitHub Repository Details

Repository vercel-labs/json-render · default branch main · size 5868 KB · watchers 59 · source: GitHub REST API and repository README

README

json-render

The Generative UI framework.

Generate dynamic, personalized UIs from prompts without sacrificing reliability. Predefined components and actions for safe, predictable output.

https://github.com/vercel-labs/json-render/blob/HEAD/Vercel Labs Product https://github.com/vercel-labs/json-render/blob/HEAD/npm version: @json-render/core https://github.com/vercel-labs/json-render/blob/HEAD/License: Apache-2.0 https://github.com/vercel-labs/json-render/blob/HEAD/npm downloads per month: @json-render/core

# 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

Why json-render?

json-render is a Generative UI framework: AI generates interfaces from natural language prompts, constrained to components you define. You set the guardrails, AI generates within them:

Quick Start

1. Define Your 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. Define Your Components

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

const { registry } = defineRegistry(catalog, { components: { Card: ({ props, children }) => (

{props.title}

{children}
), Metric: ({ props }) => (
{props.label} {format(props.value, props.format)}
), Button: ({ props, emit }) => ( emit("press")}>{props.label} ), }, });

3. Render AI-Generated Specs

function Dashboard({ spec }) {
  return ;
}

That's it. AI generates JSON, you render it safely.

---

Packages

| Package | Description | | --------------------------- | ---------------------------------------------------------------------- | | @json-render/core | Schemas, catalogs, AI prompts, dynamic props, SpecStream utilities | | @json-render/react | React renderer, contexts, hooks | | @json-render/vue | Vue 3 renderer, composables, providers | | @json-render/svelte | Svelte 5 renderer with runes-based reactivity | | @json-render/solid | SolidJS renderer with fine-grained reactive contexts | | @json-render/shadcn | 36 pre-built shadcn/ui components (Radix UI + Tailwind CSS) | | @json-render/shadcn-svelte| 36 pre-built shadcn-svelte components (Svelte 5 + Tailwind CSS) | | @json-render/react-three-fiber | React Three Fiber renderer for 3D scenes (20 built-in components, including GaussianSplat) | | @json-render/react-native | React Native renderer with standard mobile components | | @json-render/next | Next.js renderer — JSON becomes full apps with routes, layouts, SSR | | @json-render/tanstack-start | TanStack Start renderer — full apps with routes, layouts, SSR, and head metadata | | @json-render/remotion | Remotion video renderer, timeline schema | | @json-render/react-pdf | React PDF renderer for generating PDF documents from specs | | @json-render/react-email | React Email renderer for HTML/plain-text emails from specs | | @json-render/ink | Ink terminal renderer with built-in components for interactive TUIs. | | @json-render/image | Image renderer for SVG/PNG output (OG images, social cards) via Satori | | @json-render/directives | Pre-built custom directives — $format, $math, $concat, $count, $truncate, $pluralize, $join, $t (i18n) | | @json-render/codegen | Utilities for generating code from json-render UI trees | | @json-render/devtools | Framework-agnostic devtools core — panel UI, event store, picker, stream taps | | @json-render/devtools-react | React adapter for @json-render/devtools (drop-in ``) | | @json-render/devtools-vue | Vue adapter for @json-render/devtools | | @json-render/devtools-svelte | Svelte adapter for @json-render/devtools | | @json-render/devtools-solid | SolidJS adapter for @json-render/devtools | | @json-render/redux | Redux / Redux Toolkit adapter for StateStore | | @json-render/zustand | Zustand adapter for StateStore | | @json-render/jotai | Jotai adapter for StateStore | | @json-render/xstate | XState Store (atom) adapter for StateStore | | @json-render/mcp | MCP Apps integration for Claude, ChatGPT, Cursor, VS Code | | @json-render/yaml | YAML wire format with streaming parser, edit modes, AI SDK transform |

Renderers

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

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

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

Solid (UI)

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

const { registry } = defineRegistry(catalog, { components: { Card: (renderProps) =>

{renderProps.children}
, Button: (renderProps) => ( renderProps.emit("press")}> {renderProps.element.props.label as string} ), }, });

;

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, }, });

;

React Native (Mobile)

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: {} }); ;

Remotion (Video)

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: [] }, };

;

React PDF (Documents)

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 (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, }, });

;

Next.js (Full Apps)

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 // // {children} //

TanStack Start (Full Apps)

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: () => , pendingComponent: StartLoading, errorComponent: StartErrorBoundary, notFoundComponent: StartNotFound, });

Wrap the root route's outlet with `` so route fallback components can resolve the current route. Pass named $computed implementations through its functions prop.

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

Devtools

Drop-in inspector panel for any json-render app. Spec tree, state editor, action log, stream log, catalog browser, DOM picker.

// React
import { JsonRenderDevtools } from "@json-render/devtools-react";

;

Floating toggle appears bottom-right. Hotkey: Ctrl/Cmd + Shift + J. Tree-shakes to null in production.

Available for React, Vue, Svelte, and Solid — swap @json-render/devtools-react for the adapter that matches your renderer.

Ink (Terminal)

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: [], }, }, };

;

Features

Streaming (SpecStream)

Stream AI responses progressively:

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

const compiler = createSpecStreamCompiler();

// 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 Generation

Generate system prompts from your catalog:

const systemPrompt = catalog.prompt();
// Includes component descriptions, props schemas, available actions

Conditional Visibility

{
  "type": "Alert",
  "props": { "message": "Error occurred" },
  "visible": [
    { "$state": "/form/hasError" },
    { "$state": "/form/errorDismissed", "not": true }
  ]
}

Dynamic Props

Any prop value can be data-driven using expressions:

{
  "type": "Icon",
  "props": {
    "name": {
      "$cond": { "$state": "/activeTab", "eq": "home" },
      "$then": "home",
      "$else": "home-outline"
    },
    "color": {
      "$cond": { "$state": "/activeTab", "eq": "home" },
      "$then": "#007AFF",
      "$else": "#8E8E93"
    }
  }
}

Expression forms:

Actions

Components can trigger actions, including the built-in setState action:

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

The setState action updates the state model directly, which re-evaluates visibility conditions and dynamic prop expressions.

State Watchers

React to state changes by triggering actions:

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

watch is a top-level field on elements (sibling of type/props/children). Watchers fire when the watched value changes, not on initial render.

---

Demo

git clone https://github.com/vercel-labs/json-render
cd json-render
pnpm install
pnpm dev

GitHub Stars & Activity

16,967Stars
905Forks
103Open issues
TypeScriptLanguage

GitHub Popularity

GitHub stars16,967
Forks905
Open issues103
Primary languageTypeScript
LicenseApache-2.0
Stars gained today585
Created2026-01-14
Last pushed2026-09-18

Trending History

Daily boardrank #4 · ▲ 585 stars
Monthly boardrank #49 · ▲ 980 stars

Related AI Projects

1

anthropics / claude-code

TypeScript★ 146,908⑂ 23,988▲ 483 stars
2

supermemoryai / supermemory

TypeScript★ 30,666⑂ 2,675▲ 392 stars
3

krillinai / OpenCreator

TypeScript★ 11,870⑂ 1,179▲ 317 stars
4

Tencent / BrowserSkill

TypeScript★ 5,916⑂ 417▲ 612 stars
5

KnockOutEZ / wigolo

TypeScript★ 5,341⑂ 432▲ 31 stars
6

BuilderIO / agent-native

TypeScript★ 4,978⑂ 468▲ 89 stars
7

cloudflare / mcp-server-cloudflare

TypeScript★ 4,253⑂ 523▲ 40 stars
8

diffusionstudio / editor

TypeScript★ 2,965⑂ 270▲ 81 stars

More AI Rankings