Skip to content

React

Install @gistui/react, render a streamed answer in React or Next.js, handle actions, and swap in your own components.

Updated View as Markdown

@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/catalog

Render 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.

app/api/answer/route.tsts
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();
}
app/answer.tsxtsx
"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.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close