Insights
Frontend Engineering Jan 2024

Generative UI: Building Interfaces That Reshape Themselves Per User

Static UIs assume every user wants the same thing. Generative UI builds the interface around the task at hand. Here's the architecture we used to make it work without sacrificing reliability or accessibility.

The Problem

The client's internal operations tool had thirty-seven distinct screens, most of which any given user touched maybe twice a year. New hires took three months to learn where things lived. Senior staff still missed features they'd forgotten existed.

The brief was to replace the screen-based navigation with something task-based - the user describes what they're trying to do, the interface assembles itself around that task. Generative UI, in the literal sense: the model decides what to render.

We have shipped versions of this twice. The first one was a mess. The second one works. The difference was almost entirely architectural.

What Generative UI Actually Means

The term covers a range of things, from "the AI suggests which form to open" to "the AI emits raw HTML." We mean something in between: the model selects and composes from a fixed library of vetted UI components, parameterised for the current task and user context.

The model doesn't write CSS. It doesn't invent components. It picks from a registry of components that the design system already supports, fills in their props and arranges them into a layout. The output is structured JSON, not markup. A deterministic renderer turns that JSON into a React tree.

This boundary is the entire trick. Without it, generative UI produces brittle, inconsistent, inaccessible interfaces that drift further from your design system with every model update. With it, generative UI is constrained enough to be production-viable.

The First Version's Mistakes

Our first prototype let the model emit JSX directly. It was demo-ready in a week, broken in production in three.

Three things went wrong. The output drifted from the design system within hours - the model invented prop names, used colour values outside the palette and nested components in ways the design system never intended. Accessibility regressed - generated interfaces routinely shipped without keyboard traps handled, labels associated, or ARIA roles set correctly. And testing was effectively impossible - every render was different, so visual regression and end-to-end test suites couldn't establish a baseline.

We rebuilt around a strict component contract.

The Architecture That Worked

Four pieces, each enforcing a constraint the first version didn't have.

1. A typed component registry. Every component the model is allowed to use is registered with a strict JSON schema describing its valid props and child relationships. The model emits component descriptors against that schema. Anything outside the schema is rejected at parse time.

// Example registry entry
registerComponent({
  name: "DataTable",
  propsSchema: dataTableSchema,  // zod schema, props only
  children: { allowed: ["TableAction"], max: 4 },
  a11y: { requiresLabel: true },
});

2. A planner that emits a layout tree, not markup. Given the user's intent and context, the model returns a tree of component descriptors. The renderer walks the tree and mounts real React components. The model never sees rendered output; it sees the same structured input format as a backend API would.

3. Accessibility as a render-time invariant. The renderer enforces accessibility rules independently of the model. If a DataTable is emitted without a label, the renderer attaches a generated one and logs a warning. The model can't ship an inaccessible interface even if it tries.

4. A safe-defaults fallback. When the planner output is invalid, ambiguous, or fails schema validation, the renderer falls back to a deterministic default layout for the task type. Users never see a broken interface; the worst case is a slightly less personalised one.

What We'd Do Differently

We would build the component registry before any model work. Our first version treated the registry as an afterthought and the model's outputs constantly outran the components we'd actually built. Treating the registry as the contract - and only adding components when there was a clear use case - kept the surface area manageable.

We would also instrument the planner's choices heavily. Knowing which components the model picks, for which intents, in which contexts, is essential for improving the system. We started this late and lost two months of useful signal.

Takeaway

Generative UI works when the model's job is selection and composition, not generation. Lock down the component registry, enforce accessibility at render time and treat the planner's output as structured data flowing through a deterministic renderer. The "AI generates the interface" framing is appealing but misleading - the interface is built from pre-vetted parts. The AI just decides which parts and in what order.