Generative UI: When the Model Picks the Component
Chat interfaces render text. The next step is letting the model decide that this response should be a table, that one a chart, this one a form with prefilled fields, and rendering the right component. That is generative UI, and it is useful: a user asking for a comparison gets a comparison, not a paragraph describing one.
It's also a new way to get hacked and a new way to break your UI, because you have handed layout decisions to a component that can be wrong.
The model picks from a catalog
The rule that makes this safe: the model selects a component by name and supplies typed props. It never emits HTML, JSX, or anything else that gets read as markup. The example uses Zod.
const uiSchema = z.discriminatedUnion("component", [ z.object({ component: z.literal("comparison_table"), props: z.object({ columns: z.array(z.string()).max(6), rows: z.array(z.record(z.string())).max(50), }), }), z.object({ component: z.literal("metric_card"), props: z.object({ label: z.string(), value: z.string(), trend: z.enum(["up","down","flat"]) }), }), z.object({ component: z.literal("confirm_action"), props: z.object({ action: actionSchema, reversible: z.boolean() }), }),]); // Model output is parsed against this. Anything else is rejected and falls back to text.The catalog is finite, the props are validated, and the renderer only knows how to draw catalog components. A model that tries to render something outside the catalog gets a text fallback. There is no path from model output to arbitrary DOM. The model chooses from a menu you wrote. It never writes the menu.
Props are data, and data can be adversarial
A table cell value is a string. If it came from a retrieved document, it may contain an injection attempt or, more mundanely, content that breaks the layout. Render every prop as escaped, length-capped text. A component never interprets a prop as anything but a value.
The rule extends to links and actions. A confirm_action component's action prop goes through the same policy engine as any other agent action. The model choosing to render a confirmation button doesn't bypass approval; it triggers it.
Layout decisions need a fallback path
The model will pick wrong. It will render a chart for three data points, or a table for one row. Build the catalog so that every component degrades gracefully: a table with one row renders as a key-value list; a chart with too few points renders as metric cards. The model's choice is a hint, and the renderer applies its own judgment. A common version of this: a single-record lookup comes back as a comparison_table with one row. The renderer sees one row, downgrades it to a key-value list, and the user never sees the mis-pick.
Streaming components
Generative UI usually streams. The model emits the component name first, then the props. Render a skeleton for the component immediately, fill in props as they validate, and never render a component in a half-filled state that reads as wrong. Streaming Interfaces covers the resume and skeleton mechanics.
Evals for UI selection
Component choice is a decision, and decisions get evaluated. Evals for Production Agents covers the setup. Build an eval set of inputs with expected component types. Track selection accuracy. When a prompt or model change starts rendering tables where cards belong, the eval catches it before users do.
Skip any of that and generative UI is a rendering vulnerability with a language model attached.
Working on something like this?
Start a Conversation