Design Tokens

Engineering

A Practical Guide to Design Tokens That Survive a Rebrand

A design system token palette on screen

Most token systems break the first time the brand changes, and they break for the same reason: the tokens were named after what they looked like instead of what they were for. This guide covers the naming model that survives, the three-tier structure that supports it, and the migration path when you already have the wrong one.

Why most token systems break

A token named blue-500 is a fact about the current brand. A token named color-action-primary is a fact about the product. When the brand goes from blue to teal, the first name becomes a lie that you cannot fix without touching every consumer, and the second one requires changing a single value.

This sounds obvious written down. It is nonetheless the single most common failure in production design systems, because literal names are easier to invent at the moment you need them and the cost only arrives eighteen months later.

The three-tier model

The structure that holds up separates tokens into three layers, each of which may only reference the layer above it.

  1. Primitive tokens are the raw palette: teal-500, gray-900, space-4. They describe values and nothing else. Nothing in the product should reference these directly.
  2. Semantic tokens describe intent: color-action-primary, color-surface-raised, color-text-muted. These are what components consume.
  3. Component tokens describe a specific use: button-primary-bg, card-border-color. These exist only when a component needs to diverge from the semantic default.

Naming rules that actually hold

A semantic name should answer three questions in a fixed order: what category, what role, what state. color-action-primary-hover parses cleanly. hover-primary-action-color does not, and the inconsistency compounds across a few hundred tokens until nobody can guess a name without searching.

Rule of thumb. If you cannot infer a token's name from its purpose without looking it up, the naming scheme has already failed. Consistency matters more than elegance.

Implementing in CSS custom properties

Custom properties map onto this model almost exactly, because they cascade and can reference each other. The primitive layer goes on :root, the semantic layer references the primitives, and theme switching becomes a matter of redefining the semantic layer under a selector.

Theming without duplication

The mistake here is redefining primitives per theme. If dark mode changes gray-900 to a light value, the name is now actively misleading and every non-theme consumer breaks. Redefine the semantic layer instead: dark mode says color-surface now points at gray-900 rather than gray-0, and the primitives never move.

Handling the migration

If you already have literal names in production, do not attempt a big-bang rename. Add the semantic layer alongside the existing tokens, pointing at the same primitives. Migrate consumers component by component. Delete the literal tokens only when the last reference is gone, which will take longer than you expect and is fine.

What to measure

The health metric for a token system is not adoption percentage. It is the number of hard-coded values still present in component source. That number should trend to zero, and if it plateaus, the usual cause is a missing semantic token that people are working around rather than requesting.

Track it in CI, report it per team, and resist the urge to enforce it with a linter before the semantic layer is genuinely complete. A lint rule that blocks a legitimate use case teaches people to disable the linter.

Related Articles

4 Comments

  1. John Doe

    The section on pacing is the part I keep coming back to. Most guides talk about length, almost none about rhythm. This is the first piece that made the distinction land for me.

  2. Jane Doe

    Bookmarking this for the next time someone on my team asks why we cap line length. Having a reference beats re-arguing it every quarter.

  3. SemiColon

    Would love a follow-up covering how this holds up on mobile, where the measure is set by the viewport rather than by you.

Leave a Comment