Design tokens are the part of a design system everyone agrees on in theory and argues about in practice. The idea is clean: store your colours, spacing, and typography as named values. Reference those names everywhere. Change the value in one place, it propagates everywhere.
That’s the pitch. Here’s what happens after year one.
The naming problem nobody talks about
The first decision is also the worst: what do you call things?
Teams tend to split into two camps early on.
Camp A — semantic names:
color.background.primary
color.text.muted
color.border.interactive
Camp B — literal names:
gray.100
blue.500
red.warning
Both camps are wrong, but for different reasons. Literal names leak implementation details into every component that uses them — refactoring a “blue” into a “teal” means updating hundreds of references. Semantic names sound good until your color.background.primary needs to be three different things depending on context.
The approach that actually holds up: two-tier tokens.
/* Tier 1 — primitives (never used directly in components) */
--primitive-green-500: #5C8C3E;
/* Tier 2 — semantic (what components reference) */
--color-accent: var(--primitive-green-500);
Primitives describe what the value is. Semantic tokens describe what it does. Components only ever touch semantic tokens.
Where token systems quietly rot
I’ve seen this pattern in every long-running design system: the token file starts clean, then grows a misc section. Then an overrides section. Then a legacy section that nobody deletes because “something probably uses it.”
The culprit is almost always pressure to ship. A designer needs a one-off color for a new marketing page. An engineer hardcodes #FF5733 because adding a token “takes too long.” Six months later, your system has 12 reds.
The fix isn’t better process — it’s making the right thing easier than the wrong thing:
- A token exists for everything in production (enforced by lint rule)
- Adding a token takes under a minute (one file, no ceremony)
- Tokens are reviewed in design reviews, not just code reviews
Dark mode is where theory meets reality
Token systems that look good in light mode often fall apart in dark mode. The failure mode is subtle: designers create dark mode tokens by darkening the light mode values. But darkening --color-accent: #5C8C3E gives you #2E4620 — a nearly invisible green on a dark background.
Dark mode isn’t an inverse — it’s a different color palette that happens to share the same semantic names.
The tokens I’ve landed on for this site:
/* Light */
--accent: #5C8C3E; /* olive green — strong on off-white */
/* Dark */
--accent: #7DC25C; /* lighter, more luminous — readable on near-black */
Same semantic meaning, different expression. The token system handles the swap; components don’t know it happened.
The token you always forget
After all the color and typography work, teams consistently forget one category: motion tokens.
--duration-fast: 120ms;
--duration-normal: 220ms;
--duration-slow: 400ms;
--easing-default: cubic-bezier(0.4, 0, 0.2, 1);
Without these, animation timing becomes tribal knowledge — every engineer picks their own 0.3s ease and the UI feels inconsistent in ways that are hard to articulate but immediately noticeable.
The unsexy truth about design tokens: the system itself is simple. The hard part is the governance — making sure the token file stays the canonical truth and doesn’t turn into a graveyard of --color-button-hover-light-mode-v3.
That’s a people problem more than a technical one. But good tooling makes good habits easier to keep.