Generative UI with React components
Generative UI lets a model choose from components your application already owns. You describe each component and its inputs; Hashbrown turns the model’s structured response into those trusted components. The model supplies data and layout choices. Your code still owns rendering, events, and access to application services.
Use it when different requests need different presentations: a product comparison, a summary card, or a small dashboard. If every answer uses the same layout, structured output is often enough. Start with one useful component, then grow the catalog when the feature needs another way to present information.
The model selects; your components render
Scroll horizontally to see the full diagram, or expand it.
Read the diagram
The application catalog describes allowed components and their prop schemas to the model. Model output selects a component and supplies props. Hashbrown resolves that tree against the catalog, then the application renders its own component.
Expose a useful summary card
Use the endpoint and client configuration from the Quickstart. Render the component below inside the existing HashbrownProvider.
This complete example defines the card, exposes its two text fields, and renders assistant output. The fields are text bindings, so the model cannot inject component code or HTML. No tool or application write is involved.
import { Fragment } from 'react';
import { s } from '@hashbrownai/core';
import { exposeComponent, useUiChat } from '@hashbrownai/react';
function BookCard({ title, summary }: { title: string; summary: string }) {
return (
<article>
<h2>{title}</h2>
<p>{summary}</p>
</article>
);
}
const components = [
exposeComponent(BookCard, {
name: 'BookCard',
description: 'Summarize a book with its title and a short explanation.',
props: {
title: s.string('The book title'),
summary: s.string('A short plain-text summary'),
},
children: false,
}),
];
export function BookSummary() {
const chat = useUiChat({
system: 'Summarize books using one BookCard. Keep summaries concise.',
components,
});
return (
<main>
<button
type="button"
disabled={chat.isLoading}
onClick={() =>
chat.sendMessage({
role: 'user',
content: 'Summarize Pride and Prejudice.',
})
}
>
Summarize
</button>
{chat.error && <p role="alert">The request failed.</p>}
{chat.messages.map((message, index) =>
message.role === 'assistant' ? (
<Fragment key={index}>{message.ui}</Fragment>
) : null,
)}
</main>
);
}
children: false keeps the catalog flat. Give each component a stable name and describe when it is useful. If a component performs a mutation, its event handler must check current state and permission independently of the generated props.
Explore it in the workbench
The workbench below replays a recorded run of this lesson.
Watch streamed JSON resolve into a trusted SummaryCard, then accept and revise the plan.
Recorded in the real React workbench with OpenAI requests, browser tools, Hashbrown streaming, and application state. Footage slowed to half speed for readability. Replay reads captured frames. No narration. Read the text walkthrough.
Let’s build a two-step plan. We will give the model one component, SummaryCard, and describe the props it accepts. As the response arrives, Hashbrown parses the JSON and renders our component.
I use the same plan in Tools and Structured output. Here, we’ll focus on how a model selects a component and supplies its props.
Run the same conversation
Use the panel above, or run the workbench on your machine.
The replay uses this checked React client with real provider requests. The local server keeps credentials, instructions, tools, and response schema on the server. Refresh clears the in-memory conversations and accepted plan.
Expose one trusted component
SummaryCard is ordinary application code: a title, an ordered list, and an Accept plan button. We expose that existing component with a stable name, a description, and a schema. The model chooses from this catalog; it does not write executable component code.
The following excerpt is from the checked workbench client. Open the complete client for the component implementation, application state, and acceptance guard.
const components = [
exposeComponent(SummaryCard, {
name: 'SummaryCard',
description:
'Propose a concise plan for the user to review and explicitly accept.',
props: {
title: s.string('A concise plan title'),
steps: s.streaming.array(
'The ordered plan steps',
s.string('A clear, concise action'),
),
},
children: false,
}),
];
exposeComponent() connects the schema to the component’s props. A mismatched type is a build error. children: false disallows nested components; larger catalogs can allow children explicitly.
Watch the props arrive
The title uses an ordinary s.string. The steps use s.streaming.array, so resolved items can appear while the response is still arriving. Select JSON to follow the raw response, then UI to inspect resolved props. The Resolved props section follows that selection.
A partial value is useful feedback. It is still a preview. The workbench enables Accept plan only after a fresh, successfully completed response passes its two-step plan rules. Rendering the card does not update the accepted plan.
Start and render the conversation
The checked client passes the catalog, readContext tool, system instruction, and capturing transport to useUiChat(). The Quickstart shows the smaller tool-free setup; this lesson adds context and explicit acceptance.
Assistant messages expose message.ui, which React renders inside the conversation. Keep the complete message history for follow-up turns.
Read the checked client. The workbench shell records requests and parsed output; React owns the live component renderer.
Follow the second turn
Send the first prompt and select Accept plan. In State, the application revision advances from 0 to 1. Then choose Follow-up and send “Make the second step shorter.” The tool reads the accepted plan before the model proposes its revision.
Use the bottom player to revisit the first streamed JSON, parsed output, or acceptance. The chat, inspector, and state follow the same captured moment. Raw events puts every frame on the scrubber; playback makes no provider requests and cannot accept a plan.
Grow the component catalog
Use descriptions that explain when a component is useful and what its props mean. Keep names stable. If the layout accepts children, use children: 'any' for any exposed child, or provide an explicit collection of exposed components. Those children still come from your trusted catalog.
Skillet also accepts its supported Standard JSON Schema inputs. Read Skillet schemas before substituting another schema library; unsupported constructs are rejected.
Next steps
Continue with Tools to inspect readContext, or Structured output to return the same plan as typed data. See Streaming for string, array, and object streaming, and the useUiChat() API for the complete options.
