Skip to content
Generative UIGenerative UI rendererMenu

Generative UI renderer

Renders a JSON spec from the model as chatcn widgets: validated with Zod, drawn while it streams, and safe when the model gets it wrong. Accepts A2UI-style trees too.

An assistant answer streams a fare chart, a flight comparison and a booking confirmation between paragraphs.

Installation

$ pnpm dlx shadcn@latest add https://chatcn.dev/r/chatcn-genui.json

Usage

Give the model a render_ui tool whose input is a spec, then render the tool part with GenUI. While the call streams, pass the raw input text: the renderer parses it tolerantly and draws what has arrived.

tsx
import { GenUI, genuiJsonSchema } from "@/lib/chatcn-genui"
import { genuiFromPart } from "@/hooks/use-chatcn-genui-spec"

// Server: describe the tool with the same schemas the renderer validates against.
const tools = {
  render_ui: tool({
    description: "Show a chart, comparison, form, calculator or confirmation in the answer.",
    inputSchema: jsonSchema(genuiJsonSchema()),
  }),
}

// Client: render the tool part where it appears in the message.
function RenderUi({ part }: { part: ChatcnToolPart }) {
  const ui = genuiFromPart(part)
  return ui ? <GenUI spec={ui.spec} streaming={ui.streaming} onAction={handleAction} /> : null
}

<MessageParts message={message} components={{ tool: RenderUi }} />

A spec is one widget, { type, id?, props }, or a stack of widgets with direction, gap and columns. Widget types: chart, stat, stats, comparison, form, calculator, carousel, map, weather, timeline, confirmation, text and button. Their props mirror the components on the pages that follow.

json
{
  "type": "chart",
  "props": {
    "type": "bar",
    "title": "CI build time",
    "data": [{ "label": "#1187", "value": 268 }, { "label": "#1188", "value": 251 }],
    "unit": "s"
  }
}

Examples

Streaming spec

While the JSON streams, every field that already parses is kept and the newest item of each array is held back until the next one starts. A number that is still being written never draws a wrong bar, and widgets get streaming so they reserve their final size.

Raw JSON arrives 18 characters at a time. Finished items render, the one being written waits, sizes hold.

A2UI input

Pass A2UI-style messages (surfaceUpdate, dataModelUpdate, beginRendering) or { components, root }. Layout components become stacks, Text becomes text with usageHint headings, Button sends a button action with its context, { path } bindings read the data model, and component names like Chart or Weather map to our widgets.

An A2UI adjacency list with literal values, a data binding and a button action, rendered with chatcn widgets.

Actions

Interactive widgets report back through one onAction callback. Return a promise from confirm and form.submit to show progress on the button; reject it to show the error and keep the widget editable.

ActionSent when
form.submitA form passes validation. Carries values
form.cancelThe user dismissed a form
confirm / cancelThe user confirmed or declined a confirmation card
selectA comparison option (with selectable) or a carousel card was picked
calculator.submitThe user sent the calculator's numbers back, with values and results
buttonA button widget or an A2UI button. Carries action and payload

Validation and errors

  • Complete specs are validated strictly. An invalid chart keeps its frame and offers the data as a table; other widgets render the props that did validate and an inline note with the first failing path, like props.data.3.value: Expected number.
  • Unknown widget types render a short error listing the supported ones, never a blank gap.
  • Each widget sits in its own error boundary, so one broken widget cannot take down the answer around it.

API

ExportNotes
GenUIspec, streaming, onAction, renderers to override or add widget types
resolveGenUI(input, { streaming })Pure resolver from text, object or A2UI to a render tree. Works on the server and in tests
genuiSchemasZod props schema per widget type
genuiSpecSchemaThe full spec schema
genuiJsonSchema(type?)JSON Schema for a tool definition, for the whole spec or one widget
fromA2UI(messages)A2UI messages or { components, root } to a chatcn spec
useChatcnGenUISpec, genuiFromPartMemoized resolving, and reading a spec from a tool or data-genui part

Accessibility

  • Widgets keep their own semantics: charts carry a table alternative, forms label every control, the confirmation card names its status in a live region.
  • Streaming widgets set aria-busy until their spec completes.

Motion

  • The renderer adds no motion of its own. Widgets reveal new items once as they arrive, and nothing replays on re-render.