I've built the same token system three times, at three companies, and gotten it wrong in a new way each time.
The failure mode is never "we didn't use tokens." Everyone uses tokens now — Figma variables and Tailwind config make that the path of least resistance. The failure mode is a token system that technically works at launch and quietly stops scaling once the product has real breadth: a marketing site, a dashboard, a mobile web view, and a design team bigger than one person.
Here's what I've learned separates the two.
Semantic Tokens, Not Palette Tokens
The first version of almost every token system is a palette: blue-500, gray-100,
red-600. It feels organized because it's alphabetized and it maps cleanly to a Figma
color style. It's also the version that breaks first, because a component styled with
blue-500 has no idea why it's blue. Is it a link? A primary action? An info banner?
When the brand color changes, or dark mode needs a different blue, there's no way to
retarget just the right usages — you're grepping the codebase hoping you find them all.
The fix is one layer of indirection: semantic tokens that reference the palette, and components that only ever reference the semantic layer.
/* palette — the only place raw values live */
--blue-500: #3b82f6;
--gray-900: #18181b;
/* semantic — what components actually use */
--primary: var(--blue-500);
--foreground: var(--gray-900);// components reference intent, never a raw palette value
<button className="bg-primary text-primary-foreground">This is exactly what AGENTS.md means by "use semantic tokens, not raw values" — it's not a style preference, it's the difference between a rebrand being a two-line diff or a three-week migration.
Tokens Need a Contract, Not Just a Value
A token system scales when every token answers three questions on its own: what is this for, what can safely change about it, and what would break if it did. Most token systems only answer the first one, informally, in a Figma page nobody keeps updated.
I've started treating every semantic token like a tiny API:
| Token | Purpose | Contract |
|---|---|---|
--foreground | Default text color | Must hit 4.5:1 against --background in both themes |
--muted | De-emphasized surface | Never used for text directly — only backgrounds |
--ring | Focus indicator | Must stay visible against every surface token |
Without that contract, someone eventually uses --muted as a text color because it
"looked fine on my screen," and six months later there's a contrast failure buried in a
settings page nobody audits. The contract turns that from a judgment call into a
lint-able rule.
Spacing and Type Scales Need to Be Closed Systems
Color tokens get the attention. Spacing and type scales are where I actually see teams
lose control fastest, because Tailwind and Figma both make it trivially easy to reach
for an arbitrary value under deadline pressure — mt-[13px], a one-off text-[15.5px].
Each one feels harmless. A hundred of them later, the type scale isn't a scale anymore,
it's a Pareto distribution ranging from 12px to 47px with a hundred outliers, and no one
can say with confidence what the actual scale is.
The fix isn't discipline — discipline doesn't survive a deadline. It's making the closed system easier to use than the escape hatch:
- Extend
tailwind.config.tswith every spacing and type value the design system actually needs, so there's rarely a legitimate reason to reach for an arbitrary value - Turn on a lint rule that flags arbitrary Tailwind values in review, so the shortcut is visible before merge, not archaeology six months later
- When a real gap shows up — a value that's genuinely missing — add it to the scale instead of the component. The gap is signal that the system needs to grow, not that this one place gets to break the rule
The Token System Has to Own Both Themes From Day One
Retrofitting dark mode onto a token system built for one theme is close to a rebuild, because a single-theme system almost always has raw values leaking in around the edges — an icon color that was never tokenized, a border that was hardcoded because it "matched the mockup." Every one of those becomes a bug the day dark mode ships.
Building both themes from the first token — even if dark mode won't ship for months — costs almost nothing up front and saves the entire retrofit later:
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
}Every component built against semantic tokens gets dark mode for free. Every component built against a raw hex value becomes a bug ticket.
What Doesn't Scale, Even With Perfect Tokens
Tokens solve consistency of values. They don't solve consistency of usage — that's a
component-library problem, not a token problem, and conflating the two is where a lot of
token systems overreach. I've seen teams try to encode every layout decision as a token
("spacing between a card title and description must be --space-card-gap") and end up
with a token list longer than the component list it was meant to simplify.
The token layer should stay small and general — color, spacing, radius, type, motion duration. Anything more specific than that belongs in the component itself, expressed as a sensible default, not a new named token. A token system that tries to describe every layout decision in the product isn't a design system anymore. It's a second, harder-to-read version of the component library, maintained in parallel, and it will drift from the real one within a quarter.