Documentation Index
Fetch the complete documentation index at: https://langchain-5e9cc07a-preview-cbuipl-1779916257-33d1bcf.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
Generative UI lets the AI generate complete user interfaces from natural language
prompts. Instead of rendering text responses in chat bubbles, the AI output is
the UI: forms, cards, dashboards, and more. The developer defines which components
are available (the “catalog”), and the AI composes them into a valid UI tree.
This pattern uses json-render, the Generative UI framework,
to define component catalogs, generate specs with AI, and render them safely across
React, Vue, Svelte, and Angular.
How it works
- Define a catalog: declare what components the AI can use, with typed props
- Prompt the AI: describe the UI you want in natural language
- AI generates a spec: a JSON document describing the component tree
- Render safely: json-render’s
Renderer renders the spec using your components
The catalog acts as a guardrail: the AI can only use components you’ve defined,
with props that match your schema. The output is always predictable and safe.
Define a component catalog
The catalog describes every component the AI is allowed to use. Each component has a
Zod schema for its props and a description that the AI reads to understand when to
use it:
import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/react/schema";
import { z } from "zod";
const catalog = defineCatalog(schema, {
components: {
Card: {
description: "A card container with optional title and padding",
props: z.object({
title: z.string().optional(),
padding: z.enum(["sm", "md", "lg"]).optional(),
}),
},
Stack: {
description: "Layout children vertically or horizontally with consistent spacing",
props: z.object({
direction: z.enum(["vertical", "horizontal"]).optional(),
gap: z.enum(["sm", "md", "lg"]).optional(),
}),
},
TextInput: {
description: "A text input field with optional label and placeholder",
props: z.object({
label: z.string().optional(),
placeholder: z.string().optional(),
type: z.enum(["text", "email", "password", "number", "textarea"]).optional(),
}),
},
Button: {
description: "A clickable button with label and style variants",
props: z.object({
label: z.string(),
variant: z.enum(["primary", "secondary", "ghost", "link"]).optional(),
fullWidth: z.boolean().optional(),
}),
},
},
actions: {},
});
Keep catalogs focused. Include only components the AI needs for the use case.
A smaller catalog produces better results than a kitchen-sink approach.
Build a component registry
The registry maps each catalog component to its actual rendering implementation.
Use defineRegistry to get type-safe bindings between the catalog props and
your component functions:
import { defineRegistry, Renderer, JSONUIProvider } from "@json-render/react";
const { registry } = defineRegistry(catalog, {
components: {
Card: ({ props, children }) => (
<div className="card">
{props.title && <h2>{props.title}</h2>}
{children}
</div>
),
Stack: ({ props, children }) => (
<div className={`stack stack-${props.direction ?? "vertical"} gap-${props.gap ?? "md"}`}>
{children}
</div>
),
TextInput: ({ props }) => (
<div>
{props.label && <label>{props.label}</label>}
<input type={props.type ?? "text"} placeholder={props.placeholder} />
</div>
),
Button: ({ props }) => (
<button className={props.variant ?? "primary"}>
{props.label}
</button>
),
},
});
Connect to the agent
The agent uses structured output to return a json-render spec. Set up useStream
with your agent’s assistant ID, then extract the spec from the AI message’s
tool_calls:
import { useStream } from "@langchain/react";
import { AIMessage } from "langchain";
function GenerativeUI() {
const stream = useStream<typeof myAgent>({
apiUrl: "http://localhost:2024",
assistantId: "generative_ui",
});
const aiMessage = stream.messages.find(AIMessage.isInstance);
const rawSpec = aiMessage?.tool_calls?.[0]?.args;
// ... filter and render (see streaming section below)
}
Stream and render progressively
During streaming, the spec is built up incrementally. Elements arrive one at a
time and may initially lack type or props. Filter to only complete elements
and pass loading={true} to the Renderer, which tells it to silently skip
children that haven’t arrived yet. The UI builds up component by component:
/*
* Filter the streamed spec to only include elements with valid type/props,
* enabling progressive rendering as the AI response builds up. Passing
* loading={true} to the Renderer tells it to skip missing children silently.
*/
const spec = (() => {
if (!rawSpec?.root || !rawSpec?.elements) return null;
const rootEl = rawSpec.elements[rawSpec.root];
if (!rootEl?.type || rootEl?.props == null) return null;
const safeElements = {};
for (const [key, el] of Object.entries(rawSpec.elements)) {
if (el?.type && el?.props != null) {
safeElements[key] = el;
}
}
return { root: rawSpec.root, elements: safeElements };
})();
return (
<>
{spec && (
<JSONUIProvider registry={registry}>
<Renderer spec={spec} registry={registry} loading={stream.isLoading} />
</JSONUIProvider>
)}
</>
);
The JSONUIProvider is required to set up json-render’s internal context
providers (state, visibility, validation, actions). The Renderer component
must be rendered inside it.
The AI agent generates a flat JSON spec with a root key pointing to the
root element and an elements map containing all components:
{
"root": "login-card",
"elements": {
"login-card": {
"type": "Card",
"props": { "title": "Login" },
"children": ["login-stack"]
},
"login-stack": {
"type": "Stack",
"props": { "direction": "vertical", "gap": "md" },
"children": ["email-input", "password-input", "submit-btn"]
},
"email-input": {
"type": "TextInput",
"props": { "label": "Email", "placeholder": "Enter your email", "type": "email" },
"children": []
},
"password-input": {
"type": "TextInput",
"props": { "label": "Password", "placeholder": "Enter your password", "type": "password" },
"children": []
},
"submit-btn": {
"type": "Button",
"props": { "label": "Sign In", "variant": "primary", "fullWidth": true },
"children": []
}
}
}
Each element references its children by ID, and leaf elements like TextInput
and Button have empty children arrays.
Best practices
- Use descriptive component descriptions: the AI uses these to understand when
to use each component. Clear descriptions lead to better UI generation.
- Validate before rendering: always check that elements have valid
type and
non-null props before passing to the Renderer, since streaming delivers partial data.
- Design for streaming: pass
loading={true} during streaming so the Renderer
gracefully handles children that haven’t arrived yet. Users see the UI build up
in real time rather than waiting for the full response.
- Style with design tokens: use CSS custom properties so rendered components
adapt to light and dark themes automatically.
- Wrap with JSONUIProvider: the
Renderer must be inside a JSONUIProvider
to access json-render’s internal context for state, visibility, and actions.