Frontend Architecture
Design tokens: one source of truth before any rebrand
Marketing approves a rebrand: the brand blue moves from #0052CC to a warmer #1A66FF. The estimate is “a day.” It takes eleven weeks. The hex is hardcoded in 340 web components, a Swift UIColor extension, an Android colors.xml, two email templates, and a Figma library nobody can sync. The grep finds 18 different spellings of “the same” blue — #0052cc, #0052CC, rgb(0,82,204), rgba(0, 82, 204, 1). Dark mode has its own forked copies. Three of them get missed and ship a two-tone button to production.
A token is a decision with a name, not a value with a name
A design token is a named entry that holds a design decision — a color, a space, a radius, a font size — so that everything downstream references the name, never the raw value. The point is not to alias #0052CC to blue600; that alone saves nothing. The point is that the name carries intent, and intent is what you change during a rebrand, a theme switch, or an accessibility fix.
The failure the hook describes is what happens without tokens: the raw value is copied into every consumer, so the value is the source of truth, replicated hundreds of times. There is no single thing to edit. A token inverts that — one definition, many references — which is the same single-source-of-truth move you make with derived state, applied to design decisions instead of runtime state.
Primitive, semantic, component: the three tiers
The naive version — alias every hex to a name and stop — fails the moment you add dark mode, because blue600 is the wrong thing to reference from a button. You don’t want “the button uses blue 600.” You want “the button uses the interactive color,” and that maps to blue 600 in light mode and something brighter in dark. The fix is three tiers, each referencing the one below:
- Primitive (global) tokens — the raw palette:
color.blue.600 = #0052CC,space.4 = 16px. No meaning, just options. A component should never reference these directly. - Semantic (alias) tokens — decisions with intent:
color.interactive.default = {color.blue.600},color.surface.base,color.text.primary. This is the layer themes override. - Component tokens — the most specific:
button.background = {color.interactive.default}. Optional, but they let one component diverge without touching the semantic layer.
| Tier | Example | Carries | Who references it |
|---|---|---|---|
| Primitive | color.blue.600 = #0052CC | A raw value, no meaning | Only semantic tokens |
| Semantic | color.interactive.default = {color.blue.600} | Intent / role | Components & themes |
| Component | button.background = {color.interactive.default} | A single component’s binding | One component |
Why this works
The tell that a system is missing the semantic tier: a grep for var(--color-blue-600) returns hits inside button, link, and tab styles. That means a rebrand or a dark theme has to touch every one of those, and the palette has leaked into product code. Components referencing primitives is the single most common reason a “tokenized” system still can’t rebrand cheaply.
The pipeline: one source, many platforms
Tokens are authored once — increasingly in the W3C Design Tokens Community Group (DTCG) format, which reached its first stable version, v2025.10, in October 2025. It’s plain JSON: every token is an object with a $value and a $type, aliases use {group.token} reference syntax, and the file uses the .tokens.json extension and application/design-tokens+json media type. Composite types (typography, shadow, border, gradient) bundle several sub-values into one token.
That JSON is the single source. A build tool — Style Dictionary is the reference implementation — reads it and transforms it per platform. Transforms are the load-bearing part: the same space.4 token becomes --space-4: 1rem; for CSS (px→rem), space_4 (snake_case) in an Android colors.xml, and spaceFour (camelCase) in Swift. A color becomes #0052CC for CSS but a UIColor(red:green:blue:alpha:) for iOS and an 8-digit ARGB hex (#FF0052CC, alpha first) for Android. One token definition, three correct platform outputs — generated, not hand-copied. That generation step is exactly what the rebrand-from-hell lacked: there was no build, so every platform was a manual fork.
Order the design-token pipeline from authoring to a styled button:
- 1 Author tokens once in DTCG JSON ($value, $type, {alias} refs)
- 2 Style Dictionary reads the source and resolves all aliases
- 3 Per-platform transforms run (px→rem, hex→UIColor, name→snake_case)
- 4 Formatters emit CSS custom properties, colors.xml, a Swift file
- 5 A button references --button-background → the interactive color → blue 600
Theming and dark mode: override the semantic layer, never the hex
On the web, the output is CSS custom properties, and this is where the cascade does the work for free. You define semantic tokens on :root, then redefine the same semantic names under a theme selector — and the cascade re-resolves everything that references them, instantly, with no JS:
:root {
--color-blue-600: #0052CC; /* primitive */
--color-interactive: var(--color-blue-600); /* semantic */
}
[data-theme="dark"] {
--color-blue-600: #4D8AFF; /* same name, brighter value */
}
.button { background: var(--color-interactive); } /* never touched */The button never mentions dark mode. Flip data-theme and every component reading --color-interactive recolors in one repaint, because custom properties cascade and inherit like any other property. The senior discipline: a component must reference a semantic token, never a primitive and never a literal. The moment a button hardcodes #0052CC or even var(--color-blue-600), it falls out of the theme — and you’ve recreated the fork that dark mode was supposed to eliminate.
This is also why the cheapest enforcement is a lint rule. stylelint-plugin-no-raw-colors (or similar) makes a hardcoded hex outside the primitive file a build error. Without it, the system erodes in weeks: someone in a hurry pastes a hex, it works, it ships, and the drift restarts. Tokens are a convention, and conventions without a gate decay.
A button needs to recolor automatically in dark mode. What should its CSS reference?
The tradeoff a senior weighs: how many tiers, how soon
Three tiers is not free. Each layer of indirection is a hop a new engineer has to trace (button.background → color.interactive → color.blue.600) and a place a mistake can hide. For a five-person product with one theme and no native apps, a full three-tier system with a build pipeline is over-engineering — two tiers (primitive + semantic) as CSS variables is plenty, and you add component tokens only when a component genuinely needs to diverge.
The calculus flips hard the moment you have more than one output target — web plus iOS plus Android, or light plus dark plus high-contrast, or a white-label product with per-client themes. Each new target multiplies the cost of not having a pipeline: drift surface grows with platforms × themes. The senior read is to size the system to the number of targets you actually ship to, and to introduce the pipeline before the second target lands, not after the rebrand bills eleven weeks.
A product ships web, iOS, and Android, with light and dark themes, and a rebrand is planned for next quarter. Pick the token strategy.
- 01Why does a 'tokenized' system that aliases every hex to a name still fail to rebrand cheaply, and what's missing?
- 02How does the same token end up correct on web, iOS, and Android without anyone copying values by hand?
A design token gives a name to a design decision so every consumer references the name, never the raw value — the single-source-of-truth move applied to color, spacing, and type. The three-tier model is what makes it pay off: primitives hold the raw palette, semantic tokens carry intent and are the layer themes override, and component tokens let one component diverge. Author once in the W3C DTCG JSON format (stable as v2025.10), then let Style Dictionary transform that one source into correct CSS custom properties, iOS Swift, and Android XML — generated, never hand-copied, so platforms can’t drift. On the web, the CSS cascade does theming for free: redefine the semantic names under a theme selector and every component recolors in one repaint, provided components reference semantics and never primitives or literals. Enforce that with a lint rule, size the tiers to the number of targets you actually ship, and the rebrand that took eleven weeks becomes one edit.