All Resources
R-35
Technology
AI2UI: Generating Interfaces That Are Still Yours
Letting a model assemble the interface without letting it invent one — a constrained component registry, schema-validated intent, and the deterministic renderer in between.
PAR2 Labs
August 21, 2026
14 min

There are two ways to have a model build interface. One is to ask it for code and run what comes back, which is fast to demo and impossible to own. The other is to let it choose from components you already wrote, in a shape you already validate, and render the result yourself. Only the second survives a design system, an accessibility audit, or a security review.
01
Generate intent, not markup
The central move is to narrow what the model is allowed to produce. It does not emit JSX, HTML or CSS. It emits a small JSON document naming components from a registry you control, with props that a schema validates. Everything visual stays in your codebase.
That constraint is what makes the output boring in the best sense: it inherits your tokens, your spacing, your focus rings and your keyboard behaviour, because it is your components all the way down.
Fig 1 — validation sits between the model and the renderer, and it is not optional. Anything that fails the schema is discarded and re-requested, never rendered.
02
The registry is the contract
A component registry is a whitelist with types. Each entry names a component, describes when to use it in language a model can act on, and declares its props as a schema. The registry is what you hand the model as tool definitions, and what you validate against on the way back.
Keep it small. Twelve well-described components produce better interfaces than sixty vague ones, because selection accuracy falls off as the option space grows and the descriptions blur together.
| Registry field | Purpose | Failure if omitted |
|---|---|---|
| name | Stable identifier the model selects by | Model invents component names that resolve to nothing |
| description | When to use it, in decision language | Plausible but wrong component choice — a table where a chart was needed |
| propsSchema | Zod/JSON Schema for every prop | Unvalidated props reach render and crash on the client |
| slots | Which children are permitted | Arbitrary nesting, unbounded depth, layout that breaks |
| dataBinding | Which server fields may populate it | Model fabricates values instead of binding real data |
| maxInstances | Cap per response | Twelve charts where one was wanted |
The registry is the security boundary as well as the design boundary — treat additions to it as you would a new public API.
A model that cannot express an invalid interface will not produce one.
03
Building it
This is the whole implementation path. It is less code than it sounds; the discipline is in refusing to widen the interface when a case does not fit.
01
Define the component registry
TOOL
Zod + your design system
USE
One entry per component, with description and props schema
GET
A typed, enumerable registry
Start from components that already exist and are already accessible — shadcn/ui or Radix primitives wrapped in your tokens. Write each description as a decision rule: 'use when comparing a small number of categorical values', not 'a bar chart'.
Why: The model chooses by description. Descriptions written for humans read fine and select badly; descriptions written as decision rules select correctly.
02
Expose the registry as tools, not as prose
TOOL
Structured tool use
USE
One tool per component, or one emit_ui tool with a discriminated union
GET
Model output constrained at generation time
Give the model the registry as tool definitions with JSON Schema, so the constraint is applied while it generates rather than checked afterwards. Prefer a discriminated union keyed on component name over a free-form object.
Why: Constraining at generation is far more reliable than validating after. A model that cannot express an invalid shape does not produce one.
03
Validate, then resolve
TOOL
Zod parse + registry lookup
USE
Reject unknown components, coerce nothing silently
GET
A safe render tree
Parse the returned tree. Reject any unknown component name, any prop that fails its schema, and any nesting deeper than your slot rules allow. On failure, re-request once with the validation error included, then fall back to a static layout.
Why: This is the step people skip, and it is the one that decides whether a bad generation is a wrong-looking panel or a client-side crash in front of a customer.
04
Bind data server-side
TOOL
Your existing API layer
USE
Model names a data source; the server fetches it
GET
Real values, correct permissions
Let the model reference a data binding by key — 'revenue_by_region' — and resolve that server-side against the caller's permissions. Never let generated output carry the values themselves.
Why: A model that supplies values will confidently supply plausible wrong ones. Resolving server-side also means row-level permissions are enforced by the same code path as the rest of the product.
05
Render deterministically
TOOL
React
USE
Registry map from name to component, keyed by a stable id
GET
An interface indistinguishable from hand-built
Walk the validated tree and render each node from the registry map. Give nodes stable ids so React reconciles rather than remounting between generations. No dangerouslySetInnerHTML, no dynamic imports by string, no eval.
Why: Deterministic rendering is what makes the output debuggable — the same tree always produces the same interface, and a bug is in your component rather than in a string somewhere.
06
Instrument what gets chosen
TOOL
OpenTelemetry
USE
Log component selections, validation failures, fallbacks
GET
Evidence for the next registry revision
Record which components were selected for which intents, which validations failed and why, and how often the fallback layout was used. Review weekly and either fix the description or remove the component.
Why: Selection quality is a property of your descriptions, and descriptions can only be improved from data. A component that is never chosen, or always chosen wrongly, is a registry bug.
04
What this deliberately gives up
This approach cannot produce an interface you have not already imagined. That is the trade: novelty for ownership. If you need genuine novelty, generate a candidate in a sandbox and have a human promote it into the registry — do not widen the runtime.
Non-negotiables
01
Never render model-produced markup, styles, or code. The registry is the only surface.
02
Validate before render, always, and have a static fallback for when validation fails twice.
03
Resolve data server-side against the caller's permissions — generated output never carries values.
04
Keep the registry small and its descriptions written as decision rules.
05
Accessibility lives in your components, not in the generation. If a component is not keyboard-navigable, the generated interface is not either.
05
Reference
The tool-use and JSON Schema documentation is the load-bearing reading here; the component libraries are interchangeable as long as they are accessible by default.
Primary documentation
Tool use and structured output
Constraining generation with schemas rather than validating after the fact.
JSON Schema
The vocabulary your registry compiles to.
Zod
Runtime parsing of the returned tree, with inferred types for the renderer.
shadcn/ui
Components you own outright — the right starting point for a registry.
Radix Primitives
Accessible behaviour underneath, so generated interfaces inherit it.
React
Reconciliation and keys — why stable ids matter between generations.
OpenTelemetry
Instrumenting selection quality and validation failures.
Key Takeaways
01
The model emits intent against a registry — never markup, styles or code.
02
Constrain at generation with tool schemas; validating afterwards is strictly weaker.
03
Resolve data server-side against caller permissions, so generated output carries no values.
04
Selection quality is a property of your descriptions — instrument it and revise them.
PAR2 Labs · Technology
Work With Us