@gistui/react renders GistUI programs with React 19 components. It works in Next.js, Vite, Remix and any other React setup.
npm i @gistui/react @gistui/styles @gistui/catalogyarn add @gistui/react @gistui/styles @gistui/catalogpnpm add @gistui/react @gistui/styles @gistui/catalogbun add @gistui/react @gistui/styles @gistui/catalogRender an answer
import "@gistui/styles/styles.css";
import { GistUI } from "@gistui/react";
import { ui } from "@gistui/react/ui";
<GistUI library={ui} stream={response.body} onAction={(a) => console.log(a)} />;There are two ways to feed it. Use one or the other.
| Input | When |
|---|---|
stream |
A ReadableStream or async iterable of text or bytes, for example response.body. The component owns the read loop. |
source + streaming |
A growing string you hold in state, for example from useChat. Only the appended part is parsed. |
<GistUI library={ui} source={text} streaming={isLoading} onAction={handle} />With Next.js (App Router)
<GistUI> is a client component. Put it in a file with "use client", and build the prompt in a route handler so it never ships to the browser.
import { prompt } from "@gistui/catalog";
import { convertToModelMessages, streamText } from "ai";
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({ model: "openai/gpt-5", system: prompt().text, messages: await convertToModelMessages(messages) });
return result.toTextStreamResponse();
}"use client";
import "@gistui/styles/styles.css";
import { GistUI } from "@gistui/react";
import { ui } from "@gistui/react/ui";
export function Answer({ stream }: { stream: ReadableStream }) {
return <GistUI library={ui} stream={stream} />;
}A server render (renderToString) shows the content but calls no tool; data loads after mounting.
Props
| Prop | Type | Notes |
|---|---|---|
library |
Library |
ui from @gistui/react/ui, or your extended one. Required. |
stream |
ReadableStream | AsyncIterable |
Text or bytes. |
source, streaming |
string, boolean |
A growing string and whether it is still growing. |
inline |
boolean |
The text is chat text with gistui code fences. See Chat. |
onAction |
(action) => void |
Button presses, form submits, @emit, errors. See Actions and forms. |
onError, onStateChange, onProse, onAutofix |
callbacks | Diagnostics, state changes, prose lines, and what autofix changed. |
theme |
"system" | "light" | "dark" |
Default "system". |
color |
preset name | neutral, slate, stone, rose, pink, violet, indigo, blue, teal, green, orange, amber, red. |
tokens, darkTokens |
theme tokens | Values from your own code. See Theming. |
classNames |
per-component classes | For Tailwind utilities. |
initialState |
object | Starting values for $state variables. |
lockUntil |
"done" | "ready" |
When interactive parts unlock. Default "done" (the end of the stream). |
openLinks |
boolean |
false stops @open from opening links itself. |
autofix |
boolean |
Default true. See Autofix and repair. |
Tools and safety
A program is untrusted: a model wrote it, possibly steered by content it read. It cannot run code.
| Option | What it does |
|---|---|
tools |
Read-only functions (or an MCP-style client, { callTool }). @query may call them as soon as the UI renders, so nothing here may change anything. |
mutations |
Functions that change something. They run only when a person presses a button. |
onToolCall |
Called before every tool call with { name, args, kind, nodeId }. Return false to block it. |
allowedHosts |
Hosts that images, video and backgrounds may load from, for example ["cdn.example.com", "*.example.com"]. Set it when the model sees private data. |
paused |
Stops timed data refreshes. |
More in Security and Tools and live data.
Your own components
Replace any built-in component and keep its name. The prompt does not change, so the model’s output does not either.
import type { ComponentProps } from "@gistui/react";
import { ui, useGistButton, useGistField } from "@gistui/react/ui";
function MyInput(p: ComponentProps) {
const field = useGistField(p); // name, label, required, error, locked, value, setValue
return <Input name={field.name} disabled={field.locked} aria-invalid={!!field.error} />;
}
function MyButton(p: ComponentProps) {
const b = useGistButton(p); // do: actions, submit and draft in forms, opens: dialogs, links
return <><Button {...b.buttonProps}>{b.label}</Button>{b.dialog}</>;
}
const lib = ui.extend({ Input: MyInput, Button: MyButton });See Custom components and UI libraries.
What loads when
Measured gzip, React excluded: the runtime is 32 KB; the default components’ first load is 74 KB of JavaScript plus a 29 KB stylesheet. Charts, tables, slides, pickers, code, math and diagrams load on demand.