All articles
Design systemsDesign

Naming colours so the names survive a redesign

blue-500 tells you what a colour is. button-primary-background tells you what it is for. Only one of those survives a rebrand.

· Colour & front-end editorial7 min read
Title card for the article “Naming colours so the names survive a redesign”, filed under design systems.

Every design system arrives at this problem. You start with blue-500 sprinkled through the components, then somebody changes the brand from blue to green and you are left with a variable called blue-500 that contains green.

Primitives describe, semantics prescribe

A primitive is a fact about a colour: blue-500 is a particular blue. A semantic token is a decision about usage: action-bg is whatever colour a primary action should be. The first is stable; the second is where the design lives.

:root {
  /* layer 1 — primitives. Descriptive, never used directly. */
  --blue-500: oklch(0.53 0.24 264);
  --blue-700: oklch(0.42 0.21 264);
  --grey-100: oklch(0.955 0.006 265);
  --grey-900: oklch(0.180 0.018 265);
  --red-600:  oklch(0.55 0.21 25);

  /* layer 2 — semantics. What components actually use. */
  --surface:          var(--grey-100);
  --surface-raised:   #ffffff;
  --text-primary:     var(--grey-900);
  --action-bg:        var(--blue-500);
  --action-bg-hover:  var(--blue-700);
  --border-danger:    var(--red-600);
}

The rule that makes it work: a component may never reference a primitive. If a button uses var(--blue-500) directly, the layer has been bypassed and the rebrand will miss it.

Name by role, not by appearance

AvoidPreferWhy
--light-grey-bg--surfaceStops being light in dark mode
--blue-button--action-bgStops being blue after a rebrand
--red-text--text-dangerRed is the current answer, not the meaning
--border-2--border-subtleA number carries no intent
--card-white--surface-raisedDescribes elevation, not a colour

This is what makes theming cheap

Two layers, one direction of reference. Components point at semantic tokens; semantic tokens point at primitives. Nothing ever points back up, which is what makes a rebrand a one-file change.
Components point at semantic tokens; semantic tokens point at primitives. Nothing ever points back up, which is what makes a rebrand a one-file change.

With the layers separated, a dark theme is a redefinition of the semantic layer only. Primitives stay put, components are untouched, and there is exactly one place to check when something looks wrong.

[data-theme="dark"] {
  --surface:        var(--grey-900);
  --surface-raised: oklch(0.24 0.016 265);
  --text-primary:   var(--grey-100);
  --action-bg:      oklch(0.68 0.19 264);
}

How far to take it

Two layers is usually right. Some systems add a third, component-scoped layer such as button-primary-bg pointing at action-bg. That helps on very large systems with many teams, and is overhead on anything smaller. Start with two and add the third only when you feel the absence.

A naming scheme that scales

Most token systems converge on a similar shape: what it applies to, which role it plays, and optionally what state it is in. Keeping the order consistent means a name can be read without consulting documentation.

TokenApplies toRole
colour-surfaceBackgroundThe page itself
colour-surface-raisedBackgroundCards and panels
colour-textForegroundPrimary reading text
colour-text-mutedForegroundMetadata and captions
colour-borderOutlineDividers and input edges
colour-actionBackgroundPrimary buttons
colour-action-hoverBackgroundPrimary buttons, hovered
colour-dangerBackgroundDestructive actions and errors

The test for whether a token is real

A useful check before adding one: can you describe when a designer should choose it, in a sentence, without naming a colour? If the answer is yes, it is a role and deserves a token. If the only description is that it is a slightly different blue, it is a primitive with ambitions and will be misused.

The second check is whether two existing tokens would ever need to diverge. If colour-border and colour-divider will always hold the same value in every theme you plan to ship, they are one token with two names, and the duplication will drift.

Migrating an existing codebase

Introducing tokens into a product that already has colours everywhere is the common case, and doing it all at once is rarely realistic. An incremental path that keeps the product shippable throughout works better.

  1. Inventory what is actually used. A grep for colour literals usually finds three or four times more values than anyone expected.
  2. Cluster near-duplicates. Six greys within a couple of percent of one another are one primitive, not six.
  3. Define primitives for the survivors, and leave components untouched at this stage.
  4. Add semantic tokens for the roles you can name confidently — surface, text, border, action — and migrate one component family to them.
  5. Repeat by family, so each migration is reviewable, rather than by sweeping the whole codebase in one commit.
  6. Add a lint rule rejecting new colour literals once the first family is done, so the problem stops growing while you work.

The lint rule is the step that makes the rest stick. Without it, migration and regression proceed at roughly the same rate and the work never converges.

What tokens do not solve

A token layer makes theming cheap and renaming safe. It does not make a badly chosen palette good, it does not guarantee contrast, and it cannot stop someone using colour-danger for a decorative flourish. Those remain design and review problems; tokens only ensure that when you fix them, you fix them in one place.

Frequently asked questions

What is the difference between a primitive and a semantic token?
A primitive names a value by what it looks like, such as blue-600. A semantic token names it by what it is for, such as colour-action-primary, and points at a primitive. Components should reference only semantic tokens, so changing a theme means editing the mapping rather than every component.
Is it wrong to use a primitive directly in a component?
It is a signal that a semantic token is missing. Occasionally a genuinely one-off decoration justifies it, but as a habit it defeats the purpose: the moment you theme the product, every direct primitive reference has to be found and rewritten by hand.
How many semantic tokens should there be?
As many as you have distinct roles, and no more. Start small — surface, text, border, action, and the four feedback states — and add a token only when a real component needs a colour that no existing role describes.
What should I name a token that is used in exactly one place?
By its role in that place, not by the component. Naming it after the component means the token has to be renamed if the component is renamed or reused, and it encourages a token per component rather than a shared vocabulary.
Do token names need to encode light and dark?
No, and they should not. The whole benefit is that colour-surface means the right thing in both themes. Encoding the theme in the name, as with colour-surface-light, pushes the theme decision back into every component.

Sources

Keep reading