The substrate. The piece that lets an agent reach for var(--color-action-primary) instead of hallucinating #4F46E5. W3C Design Tokens Format, semantic naming, machine-parseable types — and a 1-week migration plan that gets you from hex-soup to AI-readable.
01 — The shape that wins
Three properties that ship in the W3C spec and that every modern token tool reads: $value, $type, and $description. The first gives the agent the value. The second tells it what kind of value. The third tells it when this token applies vs another one that looks similar.
The shape (W3C-compliant)
{
"color": {
"action": {
"primary": {
"$value": "{color.blue.600}",
"$type": "color",
"$description": "Primary brand action. Buttons, primary CTAs, key navigation."
},
"danger": {
"$value": "{color.red.600}",
"$type": "color",
"$description": "Destructive action. Delete, remove, irreversible operations."
}
},
"blue": {
"600": { "$value": "#2563eb", "$type": "color" }
},
"red": {
"600": { "$value": "#dc2626", "$type": "color" }
}
}
}Notice the two layers. color.action.primary references color.blue.600 via {color.blue.600} syntax. The agent sees the semantic name first — that's the layer it picks from. The primitive is the implementation detail.
02 — Three layers
Two layers are required (primitive + semantic). The third (component-specific) is optional and used sparingly. The order matters because it's how agents disambiguate: when an agent decides what color a button background should be, it picks from semantic. When it asks what value semantic resolves to, it reads primitive. When it asks what value the button background uses, it reads component — if the layer exists.
color.blue.500Raw value. Named by appearance.
Consumed: By the system itself. Never by components directly.
color.action.primaryIntent. Named by purpose.
Consumed: By components. The layer agents reach for.
button.color.bgPer-component override. Named by surface.
Consumed: By a single component. Use sparingly.
03 — Before / after
The same component, before and after the migration. Read both as if you're an LLM that just opened the file with no other context. The "before" version forces guessing. The "after" version makes the right answer obvious.
Before · hex-soup
function Button({ variant = 'primary', children }) {
const colors = {
primary: { bg: '#3B82F6', fg: '#ffffff' },
secondary: { bg: '#F3F4F6', fg: '#111827' },
danger: { bg: '#DC2626', fg: '#ffffff' },
}
const c = colors[variant]
return (
<button style={{
background: c.bg,
color: c.fg,
padding: '8px 16px',
borderRadius: '6px',
}}>
{children}
</button>
)
}Agent perspective: Five hex values with no relationship to anything else. Adding a new variant means inventing a new hex. Changing the brand means find-replace across N files.
After · semantic tokens
type Variant = 'primary' | 'secondary' | 'danger'
function Button({ variant = 'primary', children }: {
variant?: Variant
children: React.ReactNode
}) {
return (
<button
className={`btn btn-${variant}`}
>
{children}
</button>
)
}
/* tokens.css */
.btn-primary { background: var(--color-action-primary); color: var(--color-fg-on-primary); }
.btn-secondary { background: var(--color-action-secondary); color: var(--color-fg-primary); }
.btn-danger { background: var(--color-action-danger); color: var(--color-fg-on-primary); }Agent perspective: Three named variants, three semantic tokens. Adding a new variant means adding to the union type (which the type-checker enforces) and a new token. Changing the brand means editing one variable.
04 — Anti-patterns
These come up in every audit. The first three are unintentional (no one wakes up choosing them); the fourth is usually inherited from older token systems where the W3C spec didn't exist yet.
Avoid
color: '#3B82F6'Prefer
color: var(--color-action-primary)An agent editing a hex literal has no way to know the relationship between #3B82F6 in Button.tsx and the visually-identical #3B82F6 in Link.tsx. It treats them as independent. Change one, drift the other.
Avoid
color.blue.500, color.red.600Prefer
color.action.primary, color.feedback.dangerAn agent picking from blue.300 vs blue.400 vs blue.500 is guessing. An agent picking action.primary vs action.secondary is parsing intent. The semantic layer is the difference between "agent helps" and "agent invents."
Avoid
The token catalog lives in the team's Figma Variables panelPrefer
.tokens.json checked into the repo + auto-generated CSS varsAgents can read JSON. They can't read Figma. If your tokens only exist in a design tool, the agent rebuilds them from scratch every session — usually with hallucinated names.
Avoid
{ 'color.action.primary': '#3B82F6' }Prefer
{ 'color.action.primary': { '$value': '#3B82F6', '$type': 'color', '$description': 'Primary brand interaction. Buttons, links, primary CTAs.' } }The $type tells the agent this is a color (not a string). The $description tells it when to use this token over another. Both fields ship in the W3C spec and both are how agents disambiguate at runtime.
05 — The 7-day migration
Not a multi-quarter project. Most products have 200–800 hex literals to migrate. An agent can do most of the find-and-replace once you've given it the mapping. Seven days of work splits into one day of audit, two of authoring, two of migration, and two of hardening.
Audit
Run grep -r '#[0-9a-fA-F]\{6\}' src/. Make a spreadsheet of every match. Most teams find 200–800 hex literals on their first audit. That number is your migration backlog.
Cluster
For each unique hex value, decide what role it plays: action, surface, feedback, foreground, border. Now you have your semantic layer in draft form. Don't try to be clever — 20–30 semantic tokens cover most products.
Author
W3C spec: every token has a $value, a $type, and a $description. Primitive tokens reference raw values; semantic tokens reference primitives via {color.blue.500} syntax. Check this file into the repo.
Pipe
Use Style Dictionary, Tokens Studio, or write 30 lines of TypeScript. Output: tokens.css with --color-action-primary: oklch(...) for every semantic token. Import once in your root stylesheet.
Migrate
This is the manual part. Most agents (Cursor, Claude Code) can do bulk find-and-replace if you give them the mapping. Aim for 80% coverage in one session; the long tail can wait.
Lint
Add a Stylelint or ESLint rule that fails CI on any hex literal in components. Once the rule passes, the drift problem is solved — no future PR can reintroduce hardcoded colors.
Ship
Add a README.md in your tokens/ folder that explains the layering, names the semantic categories, and shows three example consumptions. Agents will read this file when reasoning about your token catalog.
Heuristic
Don't aim for 100% coverage in week one. 80% by Friday is the target. The CI lint rule (day 6) ensures the remaining 20% can't grow — they shrink as files get touched. Within 6 weeks, the long tail is gone.
Next pillar
The next pillar is the query surface. A token catalog the agent has to grep for is still better than hex literals, but an MCP server that exposes list-tokens, find-component, and get-pattern as first-class tools is what moves agents from "guessing well" to actually using your system.