GenUI
Overview
Let a model compose real comp0 interfaces: it writes layout as JSON from a catalog of accessible components, and your server streams it to the browser as React Server Components.
What @comp0/genui is
@comp0/genui lets a model compose interfaces from @comp0/react. The model writes layout: one JSON tree that names components from a catalog. Your server validates it as it streams and renders it as React Server Components, and the browser receives HTML for the layout and predefined client components for everything interactive. It has no third-party runtime and no provider lock-in: any model that can write JSON works.
- Layout, not code: the model can only name catalog components, such as
{ "type": "Select", "label": "Plan", "name": "plan", "options": ["Basic", "Pro"] }, and pass the props their schemas declare. Each prop is a Standard Schema, the single source of the validator, the JSON Schema for structured outputs, and the prompt; the built-ins are written with ArkType and ship precompiled to plain JSON Schema, and your own components can use ArkType, zod, or any library that implements the standard. - Streams: the page fills in top-down while the model writes. Components keep their identity, typed text, and focus as the response grows, and nothing warns, moves focus, or acts until it is complete.
- Connected: tools you define run on your server for
Querynodes and for button and form actions, and your own views, placed withSlot, follow the values the model's controls set, live, without a round trip. - Safe by construction: components render only the props their schema declares, only safe URLs, and text as text. Computed values run in a small whitelisted evaluator, never
eval. - Accessible by schema: every control, chart, table, and image requires its accessible name, and a component without everything it requires is not drawn.
- Repairable: a finished response with problems yields structured errors you can send back to the model.
From model to browser
A response passes through three hands. The model writes JSON, because that is what it can stream and what a schema can hold it to. Your server turns the JSON into React Server Components with @comp0/genui/rsc. The browser receives the RSC stream: layout and text as HTML, with no JavaScript, and controls, charts, tabs, and your own views as client components it already has.
Here the server streams the heading as HTML and the select as a client component with its props, each in its own Suspense boundary, so each appears as soon as the model has written it.
// What the model writes
const response = {
type: "Stack",
children: [
{ type: "Heading", text: "Your trip" },
{ type: "Select", label: "Seat", name: "seat", options: ["Aisle", "Window"] },
],
};
// What the server streams, as React Server Components
<div data-slot="stack">
<Suspense>
<h2 data-slot="heading">Your trip</h2>
</Suspense>
<Suspense>
{/* A client component: its code is already in the browser; only the props travel. */}
<GenUIClientNode node={response.children[1]} />
</Suspense>
</div>;Install
react and react-dom 19 are peer dependencies, so your app controls their versions; there are no other runtime dependencies. comp0 ships no CSS; the output carries data-* hooks to style.
pnpm add @comp0/genui @comp0/react react react-domRender a model's answer
On a server that renders React Server Components, send genuiPrompt() as the system message and hand the model's text stream to a GenUIView. This is a React Router route; any RSC framework works the same way.
import { GenUIRoot, GenUIView, genuiPrompt } from "@comp0/genui/rsc";
import { tools } from "./tools.js";
const system = genuiPrompt({ preamble: "You are a travel assistant.", tools });
export function loader({ request }: { request: Request }) {
return { message: new URL(request.url).searchParams.get("q") ?? "Plan a weekend in Lisbon" };
}
export function ServerComponent({ loaderData }: { loaderData: { message: string } }) {
return (
<GenUIRoot call="/api/genui/tools">
<GenUIView stream={streamFromYourProvider(system, loaderData.message)} />
</GenUIRoot>
);
}Without server components
In the browser, append each streamed chunk to a string and hand the text to GenUI. Actions and repair requests come back as the next user message, so the conversation keeps going.
import { formatErrors, GenUI, genuiPrompt } from "@comp0/genui";
import { useState } from "react";
const system = genuiPrompt({ preamble: "You are a travel assistant." });
export function Assistant() {
const [response, setResponse] = useState("");
const [streaming, setStreaming] = useState(false);
async function ask(message: string) {
setResponse("");
setStreaming(true);
for await (const chunk of streamFromYourProvider(system, message)) {
setResponse((text) => text + chunk);
}
setStreaming(false);
}
return (
<GenUI
response={response}
streaming={streaming}
onAction={(action) => ask(action.message)}
onError={(errors) => ask(formatErrors(errors))}
/>
);
}Find your way
Each page covers one part of the package.
- Response format: Components, containers, data, bindings, and expressions.
- Component catalog: Every component a model may use, with its props, and your own entries.
- Prompt and schema: Teach the model the catalog, or hold it to the format with a schema.
- Server components: Stream responses as React Server Components: roots, views, your components, and tools.
- Browser renderer: GenUI props, the streaming contract, tools and host views, actions, state, and errors.
- Parser, validator, and store: The pieces GenUI is built from, for servers and tests.
- Safety, accessibility, and styling: What model output can reach, the limits, and the styling hooks.