Quick Start

Get up and running with json-render in 5 minutes.

1. Define your catalog

Create a catalog that defines what components AI can use:

// lib/catalog.ts
import { defineCatalog } from '@json-render/core';
import { schema } from '@json-render/react';
import { z } from 'zod';

export const catalog = defineCatalog(schema, {
  components: {
    Card: {
      props: z.object({
        title: z.string(),
        description: z.string().nullable(),
      }),
      slots: ["default"],
      description: "Container card with optional title",
    },
    Button: {
      props: z.object({
        label: z.string(),
        action: z.string().nullable(),
      }),
      description: "Clickable button that triggers an action",
    },
    Text: {
      props: z.object({
        content: z.string(),
      }),
      description: "Text paragraph",
    },
  },
  actions: {
    submit: {
      params: z.object({ formId: z.string() }),
      description: "Submit a form",
    },
    navigate: {
      params: z.object({ url: z.string() }),
      description: "Navigate to a URL",
    },
  },
});

2. Define your components

Use defineRegistry to map catalog types to React components. Each component receives type-safe props, children, and emit:

// lib/registry.tsx
import { defineRegistry } from '@json-render/react';
import { catalog } from './catalog';

export const { registry } = defineRegistry(catalog, {
  components: {
    Card: ({ props, children }) => (
      <div className="p-4 border rounded-lg">
        <h2 className="font-bold">{props.title}</h2>
        {props.description && (
          <p className="text-gray-600">{props.description}</p>
        )}
        {children}
      </div>
    ),
    Button: ({ props, emit }) => (
      <button
        className="px-4 py-2 bg-blue-500 text-white rounded"
        onClick={() => emit("press")}
      >
        {props.label}
      </button>
    ),
    Text: ({ props }) => (
      <p>{props.content}</p>
    ),
  },
});

3. Create an API route

Set up a streaming API route for AI generation:

// app/api/generate/route.ts
import { streamText } from 'ai';
import { catalog } from '@/lib/catalog';

export async function POST(req: Request) {
  const { prompt } = await req.json();

  // Generate system prompt from catalog
  const systemPrompt = catalog.prompt();

  const result = streamText({
    model: 'anthropic/claude-haiku-4.5',
    system: systemPrompt,
    prompt,
  });

  return result.toTextStreamResponse();
}

4. Render the UI

Use providers and the Renderer with your registry to display AI-generated UI:

// app/page.tsx
'use client';

import { Renderer, StateProvider, ActionProvider, VisibilityProvider, ValidationProvider, useUIStream } from '@json-render/react';
import { registry } from '@/lib/registry';

export default function Page() {
  const { spec, isStreaming, send } = useUIStream({
    api: '/api/generate',
  });

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
    send(formData.get('prompt') as string);
  };

  return (
    <StateProvider initialState={{}}>
      <VisibilityProvider>
        <ActionProvider handlers={{
          submit: (params) => console.log('Submit:', params),
          navigate: (params) => console.log('Navigate:', params),
        }}>
          <ValidationProvider customFunctions={{}}>
            <form onSubmit={handleSubmit}>
              <input
                name="prompt"
                placeholder="Describe what you want..."
                className="border p-2 rounded"
              />
              <button type="submit" disabled={isStreaming}>
                Generate
              </button>
            </form>

            <div className="mt-8">
              <Renderer spec={spec} registry={registry} loading={isStreaming} />
            </div>
          </ValidationProvider>
        </ActionProvider>
      </VisibilityProvider>
    </StateProvider>
  );
}

Quick Start with shadcn/ui

If you want to skip defining components from scratch, use @json-render/shadcn for 36 pre-built components:

// lib/catalog.ts
import { defineCatalog } from '@json-render/core';
import { schema } from '@json-render/react/schema';
import { shadcnComponentDefinitions } from '@json-render/shadcn/catalog';

export const catalog = defineCatalog(schema, {
  components: {
    Card: shadcnComponentDefinitions.Card,
    Stack: shadcnComponentDefinitions.Stack,
    Heading: shadcnComponentDefinitions.Heading,
    Button: shadcnComponentDefinitions.Button,
    Input: shadcnComponentDefinitions.Input,
  },
  actions: {},
});
// lib/registry.tsx
import { defineRegistry } from '@json-render/react';
import { shadcnComponents } from '@json-render/shadcn';
import { catalog } from './catalog';

export const { registry } = defineRegistry(catalog, {
  components: {
    Card: shadcnComponents.Card,
    Stack: shadcnComponents.Stack,
    Heading: shadcnComponents.Heading,
    Button: shadcnComponents.Button,
    Input: shadcnComponents.Input,
  },
});

See the @json-render/shadcn API reference for the full component list.

Next steps