๐Ÿšง Under construction โ€” I'm migrating this site from Framer to Next.js and publishing it early for testing, so a lot of the content is still in flux.๐Ÿšง Under construction โ€” I'm migrating this site from Framer to Next.js and publishing it early for testing, so a lot of the content is still in flux.๐Ÿšง Under construction โ€” I'm migrating this site from Framer to Next.js and publishing it early for testing, so a lot of the content is still in flux.๐Ÿšง Under construction โ€” I'm migrating this site from Framer to Next.js and publishing it early for testing, so a lot of the content is still in flux.๐Ÿšง Under construction โ€” I'm migrating this site from Framer to Next.js and publishing it early for testing, so a lot of the content is still in flux.๐Ÿšง Under construction โ€” I'm migrating this site from Framer to Next.js and publishing it early for testing, so a lot of the content is still in flux.
Open menu
Switch to Darkhello@product.inc
Writing

Why generation touches tokens, never screens

ยท Alex Zapadenko

If you let a model edit individual screens, you get drift โ€” a hundred screens that are each defensible and collectively incoherent. The rule that prevents it is narrow: AI proposes the system, deterministic composition enforces it, and generation touches tokens and components but never an individual screen. The arithmetic is what makes it non-negotiable.

The arithmetic

The design system behind this site has 139 names in its token contract. Across the codebase there are 4,138 references to them โ€” about thirty per name. The single most-used, border-default, is referenced 305 times; text-default 271; primary 197.

Editing the brand's token file reaches 358 components. One JSON edit, one build step, and every one of those 4,138 references resolves to the new value โ€” consistently, everywhere, in the same commit.

Now do the same change by generating screens. The model has to find all 305 border references and treat them identically, having seen them in different files, at different times, with different surrounding context. It will get most of them. "Most" is exactly the failure: 290 correct and 15 subtly off is worse than a system with no rules at all, because the 15 are invisible until someone puts two screens side by side.

That is the whole argument. Generation at the token layer has a leverage of roughly thirty to one and is verifiable in one place. Generation at the screen layer has a leverage of one to one and is verifiable nowhere.

Drift is undetectable by review

The reason this is worth a rule rather than a preference is that screen-level drift doesn't look like a bug.

Every generated screen passes review on its own. The spacing is plausible, the grey is a reasonable grey, the corner radius looks fine. Nobody rejects it, because judged alone there's nothing to reject. The damage only exists in the relationship between screens, and design review is almost always conducted one screen at a time.

So the defect is introduced by a process that cannot see it, and discovered months later as "the product feels inconsistent" โ€” a complaint with no single owner and no single fix. By then the cost isn't a token change, it's an audit.

What each layer is allowed to do

LayerWhat generation may doEnforced by
Token valuesPropose. A human commits the change.One file; the change is reviewable as a diff
ComponentsBuild and modify, within the contractThe vocabulary โ€” there is no way to express an off-contract value
ScreensCompose from components. Never hand-edit.Composition; a screen has no styling of its own to drift

The middle row is the one that does the work, and it only works if the vocabulary is genuinely closed. In this system the rule is: if the token you need does not exist, stop and ask. Don't invent one, don't inline a value, don't compose around the limitation. That reads as bureaucratic until you notice it's the only instruction that survives contact with a model, which will otherwise happily produce a perfectly reasonable new grey.

Enforcement is the part people skip

Everything above is a slogan unless something fails when it's violated. Two mechanisms make it real here.

The first is that off-contract values simply don't render. The default Tailwind palette is switched off, so bg-blue-500 and rounded-xl emit no CSS at all โ€” an element styled that way arrives visibly unstyled rather than subtly wrong. Failure is loud, which is the only kind worth having.

The second is a check that runs over the source and flags every class that emits nothing, plus any raw colour. It's a command, it has an exit code, and it can gate a build. I ran it before publishing this piece.

If you take one thing from this: a design system that a model can follow is not a document, it's a vocabulary plus a failing test. The document part is optional.

What it costs

This makes one-off work genuinely more expensive. When a product needs something the vocabulary doesn't have, the correct move is to change the contract for everyone โ€” slower than an inline value, and occasionally the wrong trade. I've shipped things at ninety per cent because the last ten would have cost a new name.

It also has an ordering problem. The rule presumes a system exists to generate against. On a 0โ†’1 product there isn't one yet, and inventing a full token contract before you know what you're building is its own kind of waste. Early on, generate screens freely and throw them away โ€” that's what a sketch prototype is for. The rule starts mattering the moment you have more surface area than you can hold in your head, which arrives sooner than most teams expect.

Related: what scales across fifty products on the vocabulary itself, and how I work if you're building against one.