Skip to content

« all projects

gen ui demo

2026 · live · v0.4 · solo

Type a question. A chain of Claude tool-use calls fetches live data, picks components from a framework-agnostic web-component library, gives the content structure, and arranges it into a layout. Plain code runs between the model calls and enforces the design rules. The renderer is plain Svelte. The LLM never writes code.


The demo runs as a standalone app at gen-ui.jeffmills.dev. Reviewers can clone the repo and npm run dev in under a minute, with a mock mode that needs no API key.


how it works

Stage 1, data and components
The only agentic stage. The model gets one flat list of tools of two kinds. Data tools are executed: keyless public APIs for weather, exchange rates, and encyclopedia summaries. Component tools are never executed. The call is the output: the app records the tool input as a component instance and replies "rendered" so the model carries on. Around thirty component tools are generated from the Shoelace manifest, plus hand-written chart and table tools. The first round must call a tool, so the model cannot answer in prose.
Stage 2, structure and roles
A second model reorders and groups the components and marks a role on every piece of text: card title, headline figure, body, caption. It never chooses a size. If its answer is missing or malformed, stage 1's list passes through unchanged, and a rewrite that introduces a number stage 1 did not produce is rejected for that component.
Stage 3, layout
A cheaper model never sees the markup. It gets one summary line per component (id, tag, group, a density summary, and a minimum width the code computed) and must call a single layout tool whose pattern enum is generated from the layout spec. A small, structural task is why the cheaper model is enough.
Code passes, between and after
Normalization strips inline styles, invented classes, scripts, and images that did not come from a tool result. Layout repair guarantees every component appears exactly once, rows use one of the closed patterns, nothing sits in a slot narrower than its minimum, and a group stays together. Typesetting turns each role into a type style and steps down a size ladder until the text fits its slot, with one size shared across a group so cards in a row match.
Follow-ups and memory
A small final call suggests three next prompts and can never fail the request. The server is stateless: the browser sends back each earlier prompt with a short note of what was shown, and every turn fetches its data again instead of trusting numbers from an earlier one.

the layout spec: one file, four consumers

A design system's layout rules usually live in a Figma file and in designers' heads. To get a model to follow them you write them into a prompt, and to guarantee them you write them again as code. Two copies drift.

Here the rules are data. One spec file holds the grid, a closed set of six row patterns, the narrowest slot each kind of component may occupy, the grouping rules, and a table mapping text roles to type styles with a size ladder for each. Four things are generated from it: the prompt text, the layout tool's schema, the stylesheet, and the validators the repair pass runs.

rowPatterns: [
  { id: "1-up",             tracks: [1] },
  { id: "2-up",             tracks: [1, 1] },
  { id: "2-up-wide-left",   tracks: [2, 1] },
  { id: "2-up-wide-right",  tracks: [1, 2] },
  { id: "3-up",             tracks: [1, 1, 1] },
  { id: "4-up",             tracks: [1, 1, 1, 1] },
]

Change a number in the spec and the prompt and the enforcement change together. The model is asked for the pattern, and the code guarantees it. That split is the main thing I would carry into any production version: a prompt is a request, and anything that must hold needs to be checked by something that cannot be talked out of it.


roles, not sizes

Asking a model to pick font sizes produces pages where every card makes its own decision. The model is good at knowing that a string is a headline figure and bad at knowing whether it fits in a 240px slot. So the model labels meaning and code does the fitting.

<span data-role="stat-value">1 USD = 0.92 EUR</span>
<span data-role="caption">as of today</span>

The typesetting pass computes each component's slot width from the repaired layout, estimates the text's width at each step of that role's ladder, and takes the largest size that fits. Three figures in one row step down together, so the row reads as a set.


data tools shaped for a model


tested where it can be

Model output is not unit-testable. Everything around it is. The normalize, repair, and typeset passes are pure functions, and the orchestrator takes the model and the network as arguments, so the whole pipeline runs under test with a scripted model. The suite has a little over a hundred tests, including a property-style test that feeds the repair pass hundreds of random layout trees and checks that what comes out always validates against the spec. The page also has a "what the pipeline did" panel that shows each repair the code made on a live run.


what this proved

known gaps


stack

Frontend
Astro, Svelte 5 (runes), Shoelace 2.x web components, Chart.js, Tailwind v4 for tokens
Pipeline
Anthropic Messages API, multi-stage tool use with a tool-calling loop, prompt caching on tools, system prompt, and loop rounds
Layout rules
One spec file based on IBM Carbon's grid, spacing, and type tokens, generating prompt text, tool schema, CSS, and validators
Data
Keyless public APIs for weather, exchange rates, and encyclopedia summaries
Tool generation
Build-time parser over Shoelace’s custom-elements.json manifest, emitting one Anthropic tool spec per component
Tests
Vitest over the deterministic passes and the full pipeline with a scripted model
Hosting
Cloudflare Pages + Pages Functions, key in Pages env vars

Write-ups: Generating UI Without Generating Code · live demo · source