Generative UI Architecture
Why this chapter matters
Generative UI is not "the model writes React." That is the wrong mental model for production systems. A serious GenUI architecture lets an AI system propose an interface within a controlled vocabulary, while the application remains responsible for rendering, safety, accessibility, analytics, localization, authorization, and state.
The value is real: a user with a vague goal may need a table, chart, form, comparison view, calendar, approval panel, or document preview. Static UI forces every path to be predesigned. Generative UI lets the product compose the right interaction from approved parts.
Core mental model
Generative UI is component selection plus structured state, not arbitrary code generation.
The safest architecture has four parts:
| Part | Responsibility |
|---|---|
| Component registry | Defines which UI components the agent is allowed to request |
| UI schema | Describes component type, props, data bindings, events, and constraints |
| Renderer | Validates the schema and maps it to real app components |
| Interaction loop | Sends user actions and state changes back to the agent/workflow |
The model never owns the browser. It proposes a typed interface. The client validates and renders it.
Outcome-oriented design boundary
NN/g's GenUI framing is useful because it shifts the design question from "which fixed screen do we need?" to "which outcome is the user trying to reach?" That does not mean the interface should mutate without limits. A production GenUI system should adapt the work surface while keeping stable product identity, navigation, component behavior, and recovery controls.
Use this boundary:
| Adaptive | Stable |
|---|---|
| Which approved component is most useful for this task | Application shell, navigation, auth, permissions |
| Which data fields or sources are relevant | Design-system behavior and accessibility semantics |
| Which next actions should be suggested | Audit, approval, and irreversible action rules |
| Which layout best explains the result | Component registry, schemas, and analytics naming |
| Which clarification is needed | User control, editability, undo, and fallback |
If generation makes the product feel unfamiliar every time, it has crossed from helpful adaptation into instability.
Component registry design
A useful registry is more than a list of components. It is a contract.
| Registry field | Why it matters |
|---|---|
type | Stable component identifier, independent of implementation |
version | Allows migration when props or behavior change |
propsSchema | Defines allowed props, enum values, bounds, and required fields |
dataContract | Defines what data the component needs and where it comes from |
events | Defines actions the component can emit back to the agent |
permissions | Defines whether the component can show sensitive data or trigger actions |
a11yContract | Defines labels, focus behavior, keyboard behavior, and announcements |
fallback | Defines what to render if validation fails or data is missing |
Example component categories:
| Category | Examples | Architectural notes |
|---|---|---|
| Display | summary, table, card list, chart, timeline | Must support citations/provenance when content is AI-derived |
| Input | form, filter, picker, upload, date range | Must validate locally and server-side |
| Decision | approval panel, diff review, risk confirmation | Must preserve audit trail and explicit consent |
| Navigation | next-step list, related entities, drilldown | Must not leak unauthorized routes or objects |
| Feedback | progress log, status banner, generated draft | Must distinguish model output from verified facts |
UI schema pattern
A schema should express intent, not implementation details:
{
"surface": "case_review",
"components": [
{
"id": "risk-summary",
"type": "summary_panel",
"version": "1",
"props": {
"title": "Renewal risk",
"severity": "high",
"body": "The customer has three unresolved support tickets and declining usage."
},
"data": {
"citations": ["ticket:1842", "usage:customer-77"]
}
},
{
"id": "approve-email",
"type": "approval_action",
"version": "2",
"props": {
"label": "Send retention email",
"requiresConfirmation": true
},
"events": {
"confirm": "workflow.send_retention_email"
}
}
]
}
This is intentionally not JSX. The app can validate it, log it, test it, replay it, migrate it, and reject it.
Streaming architecture
GenUI should support incremental rendering, because the useful interface may emerge before the task is complete.
| Event | Frontend behavior |
|---|---|
task.started | Show task shell, cancellation, and clear status |
retrieval.started | Show context-gathering state |
component.proposed | Validate and mount placeholder or component |
component.updated | Patch approved props or data bindings |
tool.started | Show named tool progress, not raw hidden work |
approval.requested | Freeze risky action until user confirms |
task.failed | Preserve user input and last safe generated UI |
task.completed | Commit final UI state and expose next actions |
The UI should never wait for a giant final JSON blob if partial progress improves trust.
State ownership
GenUI state has multiple owners:
| State | Owner | Rule |
|---|---|---|
| Form input | User/client | Model may suggest defaults, not silently overwrite typed input |
| Workflow status | Agent runtime | Frontend renders progress and recovery states |
| Business records | Backend services | Agent must call authorized APIs, not invent state |
| Generated explanation | Model | Must be marked as generated and grounded where possible |
| UI layout | Renderer | Model proposes; renderer validates and enforces design system |
The dangerous architecture is one where the agent owns every state. The useful architecture is one where the agent proposes and orchestrates inside explicit ownership boundaries.
Accessibility and design-system requirements
Generated UI must not bypass inclusive component contracts. Every registry component should carry built-in accessibility behavior:
- labels and descriptions are required for inputs
- dialogs trap focus and restore it on close
- dynamic updates use appropriate live regions
- tables expose headers and captions when needed
- charts provide text alternatives or data table views
- destructive actions require clear confirmation
- generated controls follow the same keyboard behavior as deterministic controls
If a component cannot satisfy this contract, it should not be available to the agent.
Security controls
Do not render model output as HTML. Avoid arbitrary CSS class injection. Validate URLs, route ids, object ids, enum values, limits, and event names. Treat generated payloads as untrusted external input.
Recommended controls:
- schema validation at the server and client boundary
- allowlisted components and events
- prop-level sanitization
- permission checks before data binding
- output size limits
- audit logs for generated actions
- human approval for irreversible or expensive operations
- CSP and Trusted Types for any rich text rendering path
Registry lifecycle and governance
A component registry should evolve like an API. If it evolves like a random list of exported React components, every model prompt becomes coupled to implementation detail.
Use a registry lifecycle:
- Candidate: a product team proposes a component for AI composition.
- Design-system review: interaction, visual style, keyboard behavior, responsive behavior, and empty states are approved.
- Security review: allowed props, URL rules, markdown/rich text rules, data sensitivity, and event permissions are defined.
- Schema publication: the component gets a stable name, version, prop schema, examples, and invalid examples.
- Eval inclusion: tests verify the model can request the component correctly and the renderer rejects malformed payloads.
- Observability: impressions, validation failures, user edits, rejects, and actions are tracked.
- Deprecation: old versions continue rendering or degrade with a clear fallback until captured workflows expire.
Registry review should be lightweight but real. A generated component is a production surface, not an internal implementation detail.
Schema versioning strategy
Generated UI payloads may live longer than a deploy. Users may reopen workflows from yesterday, mobile apps may lag behind web deployments, and traces may need replay during incidents.
Versioning rules:
| Change | Version action |
|---|---|
| Add optional prop with safe default | Minor version or same version if renderer tolerates absence |
| Rename prop | New version and migration adapter |
| Change event meaning | New version, because user actions may differ |
| Tighten enum values | New version or compatibility layer for old payloads |
| Remove component | Keep fallback renderer until old workflows expire |
| Change accessibility behavior | Review as behavior change, not cosmetic change |
Example compatibility adapter:
type ComponentPayload = {
type: string;
version: string;
props: unknown;
};
function normalizeGeneratedComponent(payload: ComponentPayload) {
if (payload.type === "approval_action" && payload.version === "1") {
return {
...payload,
version: "2",
props: {
...payload.props,
requiresConfirmation: true,
},
};
}
return payload;
}
Do not rely on the model to keep old and new versions straight. Version handling belongs in deterministic code.
Prompt-to-component contract
The prompt should not merely say "generate a good UI." It should teach the model the available UI vocabulary and constraints.
Include:
- list of allowed components and when to use each
- prop schema summaries and enum values
- examples of valid payloads
- examples of invalid payloads
- instruction to prefer simpler surfaces when uncertain
- instruction to ask clarifying questions when required data is missing
- rules for citations and source binding
- rules for risky actions and approval surfaces
Bad prompt:
Create the best UI for this task.
Better prompt:
You may propose only these components: summary_panel, source_list,
editable_draft, approval_action, comparison_table. Prefer summary_panel
plus source_list for factual answers. Use approval_action only for
external messages or record updates. Every factual claim must include
source ids. If required data is missing, propose a clarification_form
instead of inventing fields.
Prompt quality does not replace validation, but it reduces invalid output and improves UX.
Generated UI acceptance criteria
Before rendering a generated surface as successful, evaluate it against:
| Criterion | Question |
|---|---|
| Validity | Does it match schema and component registry? |
| Relevance | Does it support the user's current goal? |
| Completeness | Does it include required fields, sources, and actions? |
| Safety | Does it avoid unauthorized data and risky side effects? |
| Accessibility | Are labels, focus behavior, and announcements covered? |
| Repairability | Can the user edit or reject wrong pieces locally? |
| Performance | Can it render without blocking interaction? |
| Traceability | Can we replay how this UI was produced? |
This acceptance layer can be partly automated. The rest should appear in review checklists and evals.
Example: generated comparison table
A comparison table is a good GenUI example because it looks simple but carries many contracts:
{
"id": "plan-comparison",
"type": "comparison_table",
"version": "1",
"props": {
"caption": "Renewal plan options for Acme",
"columns": [
{ "id": "option", "label": "Option" },
{ "id": "price", "label": "Price impact" },
{ "id": "risk", "label": "Risk" },
{ "id": "source", "label": "Evidence" }
],
"rows": [
{
"id": "baseline",
"option": "Renew current plan",
"price": "No discount",
"risk": "High churn risk",
"source": ["usage:acme-q2", "ticket:8271"]
}
]
},
"constraints": {
"requiresCitations": true,
"maxRows": 5,
"emptyState": "Ask for more account context before comparing plans."
}
}
Architecture questions:
- What happens if a row lacks citation ids?
- Can the user sort or edit rows?
- Are source ids authorized for this user?
- Can mobile render the table as cards?
- Does the component provide a text alternative for screen readers?
- Does exporting this table require approval?
Anti-patterns
- Letting the model return JSX, HTML, or script.
- Creating a registry so broad that it becomes arbitrary UI execution.
- Hiding agent progress behind a spinner until completion.
- Treating generated UI as inherently accessible because the components are reused.
- Allowing generated actions to bypass normal product authorization.
- Shipping without replayable traces of what UI was proposed and why.
- Optimizing for impressive generated screens instead of task completion and repairability.
- Regenerating a whole workflow when the user only needs to fix one generated field.
Verification checklist
- Every generated component is validated against a versioned schema.
- The renderer rejects unknown components, unknown props, oversize payloads, and unauthorized events.
- Generated UI has deterministic fallbacks.
- User-entered state cannot be overwritten without consent.
- Accessibility requirements are enforced at component-registry level.
- Generated UI events are logged with workflow id, user id, model version, and request context.
Exercises
- Design a registry for five components in your product that would be safe for an agent to compose.
- Write the validation rules for a generated approval panel.
- Define how your app should recover if a generated component fails validation mid-stream.
- Compare a chat-only design with a GenUI design for the same workflow.
Source lens
This chapter is derived from the A2UI model of declarative UI descriptions, the AG-UI event-oriented agent/frontend model, NN/g's outcome-oriented GenUI framing, Google Research's custom interactive GenUI framing, and security patterns from frontend architecture chapters on browser attack surface, design-system contracts, and accessibility.