Coding agents follow a design system very well when handed one. Handed nothing, they do exactly what a competent new hire would do without a spec: they improvise plausible values. A slightly different blue. A radius of 6, because 6 looked right. A focus state invented from scratch, usually inaccessible.
The problem is not that the output is bad — it is often good — but that it is unanchored. And UI is now generated far faster than any human reviews it for system compliance, so what used to be a slow leak of inconsistency has become something else entirely.
Why agents drift by default
A coding agent sees the files you give it. If your design system exists as a Figma file and a page in Notion, it is invisible: the agent has your codebase, in which the system appears only as whatever conventions it can infer from the components that already exist. That inference is where drift enters.
- Plausible-looking values. Asked for a primary button, a model produces a good primary button — in a blue, not your blue.
- Inference from a bad sample. If three of the ten components it can see contain hardcoded hexes, it will reasonably conclude that hardcoded hexes are the convention.
- Invented intermediate steps. Needing a hover state and finding no token for one, it will darken the base colour by some amount it chooses.
- Accessibility as an afterthought. Contrast is checkable but rarely checked, and a generated focus ring is frequently below 3:1.
What to actually hand it
“Give it the tokens” is right and insufficient — a JSON dump of four hundred values is not guidance, it is a haystack. What works is a short document that states the rules and points at the values, placed where the tool reads by default.
| Tool | Reads by default | Notes |
|---|---|---|
| Claude Code | CLAUDE.md, plus skills it loads on its own | Nested files apply to their subtree — a UI-specific one can live beside the components. |
| Cursor | .cursor/rules/*.mdc | Rules can be scoped by glob, so UI rules apply only to component files. |
| GitHub Copilot | .github/copilot-instructions.md | Repository-wide; keep it short, it is prepended to every request. |
| Most others | An AGENTS.md at the repo root | The emerging common convention; cheap to provide alongside the tool-specific one. |
The five things the document must say
- Never write a raw value. No hex codes, no pixel values, no arbitrary Tailwind values — name the token instead. State this first; it is the rule that prevents the most damage.
- The token list, tiered. Primitives, semantic roles, and which tier components are allowed to reference. Include the actual names — an agent cannot use a token whose name it has to guess.
- The pairings. Which foreground goes on which surface. This is what stops invented combinations, and it carries the accessibility guarantee with it.
- The component inventory. What already exists, and where it is imported from. Most drift is a second Button, written because the first was not found.
- The states. Hover, focus, active, disabled — as named tokens. Left unstated, these are derived arithmetically and inconsistently every single time.
# UI rules
Never write a raw colour, size, radius or duration. Use a token.
## Colour roles (light / dark are handled by the variables)
surface, surface-raised, surface-sunken
fg, fg-muted, fg-subtle
action-primary + text-on-action-primary
border, border-strong, focus-ring
## Pairings that are checked and safe
fg on surface, surface-raised, surface-sunken
fg-muted on surface, surface-raised
text-on-action-primary on action-primary
## Spacing / radius
space-0 … space-24 (4px base). radius-{none,sm,md,lg,full}.
## Components that already exist — import, do not rewrite
Button, Input, Select, Dialog, Card, Table from "@/components/ui"
## States
hover → action-primary-hover
focus → 2px focus-ring, offset 2px
disabled → fg-subtle on surface-sunkenNote how little of that is prose. An agent needs the vocabulary and the constraints; the paragraphs explaining why are for humans, and in a rules file they mostly consume budget.
Keep it generated, not written
The catch with a hand-written rules file is that it is a fifth copy of the system — after the Figma file, the CSS, the config and the docs — and it will go stale exactly like the other four. Worse, a stale rules file is more damaging than none: it actively instructs the agent to use tokens that no longer exist.
So the rules file should be an output. When the token list, the pairings and the component inventory are all compiled from the same model that produces the Tailwind theme and the Figma variables, the agent guide cannot describe a system that no longer exists.
That is one of the artefacts Arkitype ships from the Ship area: an agent-readable guide to the system you just built, plus a Claude skill that loads itself when the work turns out to be UI. Re-export after a change and the guide changes with it.
Reviewing what comes back
Handing over the contract raises the floor; it does not remove the need to check. Three checks catch nearly everything, and all three are cheap enough to automate:
- Grep for raw values. A regex for
#[0-9a-f]{3,8}and for bracketed arbitrary values in the diff. One rule, catches the majority. - Check for duplicate components. A new file named like something that already exists is the tell.
- Run the contrast check on any new pairing. If the generated code introduced a foreground-and-background combination that is not on the approved list, it has not been checked by anyone.
The short version
Agents are unusually good at following an explicit system and unusually fast at inventing one when none is available. Almost nobody ships the artefact that decides which of those two happens — and it is a short document, not a platform.
Write down the tokens, the pairings, the inventory and the states; put the file where the tool already looks; and generate it from the same source the rest of your system comes from, so it is still true next month.