Tokens are a contract, not a palette

A colour token is a promise about meaning, not a hex value: treat it like code.

Most token sets start as a palette exported from a design file: blue-500, grey-200, red-700. That is useful, but it is a list of paint, not a system. A palette tells you what colours exist. It says nothing about what any of them is for.

A palette is not a system

When tokens are named after their values, two things go wrong.

The first is change. A rebrand moves the primary blue a few degrees, and now blue-500 either lies about its value or has to be renamed in every file that uses it. Both are bad; the second is also slow.

The second is meaning. Is blue-500 a link, a button, a focus ring or a chart series? The name can’t tell you, so every use is a small guess, and guesses drift. A year later the same blue is used for five things, and changing one of them changes all five.

What the contract says

A semantic token is named after what it means, not what it looks like. The name is a promise about meaning; the value is the implementation of that promise.

:root {
  --color-action: #1f4e9c;
  --color-on-action: #ffffff;
  --color-text: #111111;
  --color-text-muted: #515151;
}

.button {
  background: var(--color-action);
  color: var(--color-on-action);
}

The contract goes further than names. A pair of tokens can promise a relationship: text in --color-on-action on a --color-action background will always meet a contrast ratio of at least 7:1. Now a value change that breaks the promise is a bug you can test for, not a surprise someone finds in production.

Breaking changes

Treat tokens like any other public API.

  • Renaming or removing a token is a breaking change: deprecate first, keep the old name working for a release, then remove it.
  • Changing a value is a breaking change if it breaks a promise, such as a contrast pair or a size a layout depends on.
  • Every change gets a changelog entry that says why, not just what.

None of this needs special tooling. It needs the same discipline you already apply to a function signature.

Who signs it

A contract needs two parties. Designers decide what a token means; front-end developers decide how the meaning is delivered in code. Both sign, and one person or team owns the set and reviews changes the way a maintainer reviews pull requests.

If nobody owns the tokens, everybody does, and the design file and the codebase slowly start saying different things.

In practice

This site runs on eleven colour tokens and two themes. Every colour in the code is a token, every token has a value in both themes, and every text pairing is checked against WCAG AAA in both. Adding a colour means adding a token, a dark value and a contrast check first. It is a small set, deliberately: the fewer promises you make, the easier they are to keep.

[TEXT: an example from work: a token change that a contract would have caught, or did]