How to keep your coding agent on your design system
Coding agents write UI fast, and drift from your design system just as fast. Six checks for React teams: tokens, agent instructions, lint rules, a baseline, strictness by area, and a look at the page.
Ask a coding agent for a settings page and you'll usually get a good one. Look closer and there's a grey that isn't one of yours, a 14px gap where everything else uses 12 or 16, and a button that looks like your button but was built from a <button> and some classes.
None of it is wrong enough to fail a review. All of it is design system drift: UI that doesn't come from your design system. One page of it is nothing. A month of agent turns later you have two design systems, and the one in your token files is the one nobody follows.
I've spent twelve years watching design and code drift apart, and agents speed it up. They ship more UI, and they're happy to improvise every time. Here's what actually holds them, in the order I'd add it. None of it needs a particular tool.
1. Make the token the easy path
An agent copies what it sees. If your components are full of raw values, it writes more raw values.
Put every colour, space, radius and type size behind a custom property, and use only those in components:
/* tokens.css: the only file with raw values */
:root {
--color-text: #1a1a1a;
--color-text-muted: #5c5c5c;
--color-surface: #ffffff;
--space-2: 8px;
--space-3: 12px;
--radius-control: 6px;
}
/* Button.module.css */
.button {
padding: var(--space-2) var(--space-3);
border-radius: var(--radius-control);
color: var(--color-text);
}Name tokens by role (--color-text-muted), not by value (--grey-500). A role tells the agent when to use it. A value only tells it what it looks like, so it picks the nearest-looking one.
2. Write the rules where the agent reads them
Claude Code reads CLAUDE.md and Codex reads AGENTS.md at the start of a session, so they're the cheapest place to state the rules. Keep it short and concrete. A map beats a philosophy:
## UI rules
- Components come from `@/components/ui`. Never build a button, input or dialog from scratch.
- Colours, spacing, radius and type use the tokens in `src/styles/tokens.css`.
No raw hex, rgb or px values in components.
- If the design system doesn't have what you need, stop and ask. Don't make a one-off.This helps a lot, and it isn't enough. Instructions are advice. On a long task the agent meets a case your rules didn't cover and makes something up. You need a check that says no.
3. Check it mechanically
Lint rules turn the instructions into failures the agent can see and fix. Stylelint's built-in rules cover the styles:
{
"rules": {
"color-no-hex": true,
"color-named": "never",
"function-disallowed-list": ["rgb", "rgba", "hsl", "hsla"],
"declaration-property-unit-disallowed-list": {
"/^(margin|padding|gap)/": ["px", "rem"]
}
},
"overrides": [
{
"files": ["src/styles/tokens.css"],
"rules": {
"color-no-hex": null,
"color-named": null,
"function-disallowed-list": null,
"declaration-property-unit-disallowed-list": null
}ESLint covers the components. Two built-in rules do most of the work: one stops imports from the wrong place, the other stops hand-built elements. Add this to the config you already have, after your TypeScript and React setup:
{
files: ["src/**/*.tsx"],
ignores: ["src/components/ui/**"],
rules: {
"no-restricted-imports": ["error", {
patterns: [{ group: ["**/legacy-ui/**"], message: "Use @/components/ui." }],
}],
"no-restricted-syntax": ["error", {
selector: "JSXOpeningElement[name.name='button']",
message: "Use <Button> from @/components/ui.",
}],
},
},Then run the checks inside the agent's turn, so it fixes them before you ever see them. In Claude Code, a PostToolUse hook in .claude/settings.json runs after every edit, and exit code 2 hands the output back to the agent:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "npx stylelint 'src/**/*.css' 1>&2 && npx eslint src 1>&2 || exit 2"
}
]
}
]
}
}On a big repository, point it at the changed files instead. With Codex, name the same commands in AGENTS.md as the check to pass before a task is done.
4. Keep what's already there in a baseline
Turn these rules on in a real app and you'll get hundreds of errors on day one. Nobody fixes hundreds of errors, so the rules get switched off and you're back to advice.
Keep the old drift in a baseline instead, and fail only on new drift. ESLint has had this built in since 9.24:
npx eslint . --suppress-allThat writes eslint-suppressions.json. Commit it. From then on ESLint only reports violations beyond what the file records, and --prune-suppressions shrinks the file as you clean up. It counts per file and rule, so a file that gains one new violation reports all of them, which is fair enough.
The agent now has a clean slate it must keep clean, and you clear the old drift in your own time.
5. Choose how strict, by area
Not every page deserves the same rules. The pages your customers use every day should fail on drift. A prototype you're exploring shouldn't fail on anything.
Set severity by folder:
{
files: ["src/app/(experiments)/**/*.tsx"],
rules: { "no-restricted-syntax": "warn" },
},A later config object that sets only the severity keeps the options from the earlier one. Warnings show up in review, and errors block the merge.
6. Look at the page
Code can pass every rule and the page can still be wrong. Two components that both use the right tokens can sit 40px apart for no reason, and text can fail contrast on a token pair that was never meant to meet. Lint reads files, and people see pages. Before you merge, look at the rendered result, on more than the one page the agent was working on.
Where Fragments fits
This is the problem I'm building Fragments to solve, so here's the pitch, kept to one section.
Fragments is a Mac app where you design in code. The canvas is your running React app. It reads the components and tokens already in your code and makes them a contract your agent has to follow.
You choose how strict. Allowed lists drift and lets it through. Strict fails CI on any new drift, and drift that's already there sits in a baseline until you clean it up. Some pages can be Strict while others stay free.
Claude Code and Codex run inside it and can check their work against the contract before you see it. When something does slip through, a colour typed by hand instead of your token is boxed on the page, matched to its token, and put back in one press.
It's Mac only and waitlist-only for now. Join the waitlist for an email when your place opens.
Everything above works without it, though. Start with the tokens and the lint rules. They'll catch most of it on their own.
Keep exploring
- Introducing FragmentsSee your React app’s pages together, point at what needs changing, and direct Claude Code or Codex. Review the result before and after, with Undo.
- Explore FragmentsJoin the desktop beta waitlist. Paid plans coming soon.