AI-generated UI looks finished long before it is correct. These are field notes on the difference — four ways it goes wrong, and a fix for each.
A design system is the rulebook for how a product should look — what size a button is, how much space sits around a card. An AI can build screens from that rulebook. It gets the layout right and the details wrong: a button specified at 60 pixels comes out at 48. Plausible, proportional, wrong.
That gap is drift. On-system is its opposite — output that matches the rulebook. These notes cover the four ways drift shows up and what to do about each. Every card ends with something you can try today.
What gets missed is never layout. It is precision. Four classes of drift, each one invisible at a glance.
A control spec'd at 60px renders at 48px. Plausible, proportional, wrong — 48 is a believable height, which is exactly why nobody catches it.
Bind every dimension to a named token the AI can point at — never a number it has to retype.
A 3px stroke returns as 2px. The component is right, the geometry is almost right — and almost compounds across every surface that inherits it.
Give hairline values their own tokens. Thin details drift first, because a 1px miss looks like rendering, not error.
A tint fill quietly becomes a solid. Same hue family, different treatment — a reviewer approves it at speed, an audit rejects it on sight.
Name the treatment per state in the spec — "tint", not "light red". Adjectives are where treatments swap.
This is the one I could measure. Three isolated runs built the same five components from documentation that named zero values — 222 graded properties in total. Fifteen came out wrong. None came out missing, and not once did it say it was unsure. Every single gap got filled with something plausible.
Take one property your docs never give a number for. Regenerate and look at what came back. It will not be empty — that is the whole problem.
Better prompts did not fix it. Documentation a machine cannot misread did.
"Never do X" says what to avoid and nothing about what to do, so the model fills the gap itself — confidently, and in the same wrong place every time. The exact value is what holds. A controlled reproduction isolated the two variables: naming the value moved first-build accuracy from 93% to 100%, while rephrasing the same information from prose into directives changed nothing at all. Negation may still cost you — models do get worse at negated instructions as they scale — but the failure I could measure was the missing value.
Find one rule in your docs that names no value. Add the value, regenerate, and watch whether that property stops drifting.
If the AI retypes "48" from memory, it can misremember. If it points at one named token, there is nothing to misremember. The discipline tokens always promised, now load-bearing.
Audit one component spec for retyped numbers. Replace each with its token name. Count what you find — the count is your risk surface.
The two fixes above are about values that came out wrong. This one is worse, because the value is correct. Two of my components carry a shadow — different shadows, each used once. Neither cleared the bar for a token, because the bar is reuse. Writing the spec, the model flagged the gap and then wrote the raw number in anyway: 25 of that spec’s values point at a token, and this one is just a number. Correct, referenced by nothing, and re-emitted on every build. Failing the reuse test should mean “not a shared token yet” — not “a bare number”. Name every value; tokenise it when it turns up twice. And let the agent watch for the second time, because it reads the whole system on every pass and you do not.
Find one raw value in your specs that never earned a token. Give it a name now, even a local one — and add “tell me when a value appears in a second component” to your reviewing agent’s standing instructions. Let promotion be detected, not predicted.
The verification taught me more than the finding did. Five rules for testing without fooling yourself.
Prompts name pages and point at the system — never the correction. If the fix lands unprompted, your documentation captured it and activated it. Name it, and you have tested your prompt, not your system.
Regenerate one page from a generic "match the spec" prompt. Count what lands unprompted — that count is your documentation's real score.
They need different repairs — diagnose before you fix, or you fix the wrong layer.
Label every miss in your next review "untouched" or "touched". Untouched: make the rule fire. Touched: fix the spec.
In revision passes, models happily add what the spec defines but rarely remove what it does not. Stale UI survives every update unless subtraction is itself a positive directive.
Add one line to every revision prompt: "remove whatever the spec does not define." Watch what disappears.
A single pixel off means the token chain broke somewhere upstream — long before a human eye would catch it.
Plant one odd-valued dimension as your tripwire. Check it first on every regeneration — it audits the whole chain in one glance.
Color and token errors ripple into every downstream component. Settle upstream truth before judging components, or you will re-review everything twice.
Re-order your next review bottom-up. If the color page is wrong, stop — nothing above it is worth judging yet.
Nobody disagrees drift exists. Teams disagree on where to kill it. Six enforcement points along one pipeline — tap each to see whether it fits your stack.
Acts at the docs & tokens stage. A guideline per component, positive directives, exact values, and a stylesheet generated from the production token file — one token truth. Verification is a library diff on every regeneration.
Acts at the generation stage. Drift dies at the source: the agent uses the actual coded component instead of re-creating it. "It IS the actual design system component."
Acts at the docs → generation seam. Tell the agent where to look — tokens in JSON, YAML, CSS, plus a usage spec — instead of rebuilding components from prose. Rebuild-from-description varies run to run no matter how stringent the text.
Acts at the output stage. Generator agent plus a separate reviewer/validator agent — a common pattern in practice, and common is not a demotion: it is the most portable point on the map, because it asks nothing of your stack. A manual diff loop is the human-powered version.
Replaces the generation stage. What I would call a deterministic materializer: the AI works on a description of the system, and a deterministic factory generates the code — generation variance disappears entirely.
Acts at the design system root. Audit the production system, swap in an AI-legible token layer, confirm compatibility, ship. Authority is won by evidence, not negotiation: read-only access, a staging proof, dark mode as the diagnostic, then the CTO.
Each stack and each generation point needs its own enforcement point, or a combination. The question is never "how do I stop drift" — it is "which enforcement points can my stack actually support."
Mark the two points your stack can support today. Run them in combination — one upstream, one downstream.
Two questions still open. This is where the notes grow next.
Self-supporting documentation loops: docs that produce their own next source. If output becomes input, drift compounds quietly — unless something outside the loop stays truth. My anchor today: the library, plus a diff on every regeneration.
If your docs feed their own regeneration, name your outside anchor now. No anchor, no truth — only momentum.
The real boundary is not legacy vs greenfield — it is how token authority is held. Where it is shared, fidelity lives in the contract: one canonical, executable token layer — design owning semantic intent, engineering owning production integrity.
Write down, today, who owns semantic intent and who owns production integrity for your tokens. If two systems hold truth, drift already lives between them.
If your team kills drift at a point not shown here — or you have watched one of these six fail — I want to hear about it. The notes grow as the conversations do.