Design practice

Name the decision, not the colour

Most token systems are a colour palette with extra steps. The layer that makes them worth the effort is the one teams skip.

A grey swatch, then a bright green panel holding a name, then a component that spends it: a value, a decision, a place

A palette with extra steps

Most token systems I inherit are one layer deep. There is a file with `grey-050` through `grey-900`, `blue-500`, a spacing scale, and a build step that turns them into CSS custom properties and a Swift enum.

It looks like a design system. It solves almost nothing, because it answers the wrong question. `grey-700` tells a developer what colour to use. It does not tell them when to use it, which is the question they actually have at 4pm with a card component half built.

So they guess. Ten people guess differently, the guesses are all valid hex codes from the approved palette, and eighteen months later you have four greys doing the job of secondary text and no way to change them together.

The layer that does the work

The fix is a middle layer that names a decision rather than a value.

`text-muted` is a decision. It says: this is text, and it is subordinate. Whoever consumes it does not need to know or care that it currently resolves to `grey-700`. When the brand shifts and subordinate text needs more contrast, you change one line and every subordinate label in the product moves with it.

Three layers, each with a different job:

Three token layers with real names: primitive grey-700 which is not for use, semantic text-muted which components consume, and component card-label, with the semantic layer highlighted as the one doing the work
  1. Primitive. A raw value. `grey-700: #3f3f38`. Says nothing about use. Nobody outside the token file should reference these. 2. Semantic. A decision. `text-muted: grey-700`, `surface-raised: white`, `border-subtle: grey-200`. This is the layer components consume. 3. Component. A place. `card-label-color: text-muted`. Optional, and worth adding only where a component genuinely needs to diverge later.

The rule that makes it work is boring and absolute: application code never references a primitive. The moment a component imports `grey-700` directly, that usage is invisible to every future change, and you are back to guessing.

A token whose name is its value has not abstracted anything. It has just moved the hex code somewhere less convenient.

How to tell whether yours is real

One test. Pick a semantic token and ask what would have to be true for its value to change.

If you can answer, it is a real decision. `text-muted` would change if subordinate text needed more contrast, which is a thing that happens after an accessibility audit. Good.

If the only answer is "if the brand colour changed", it is a primitive wearing a costume. `color-brand-blue` is not semantic. `action-primary` is, because it survives the brand moving to green.

The second test is subtraction. Delete a token and see whether anyone can tell you what is now undefined. If the answer is vague, nothing was consuming it, and an unused token is worse than no token because it still has to be maintained and it still shows up in the picker.

Where token systems actually break

Not in naming. In the parts nobody budgets for.

Modes multiply everything. Light and dark are not two palettes, they are one set of semantic names with two value sets. If your semantic layer is thin, dark mode forces you to invent it under deadline, which is the worst possible time. Systems that shipped light-only usually have to be rebuilt rather than extended.

State is a token problem. Hover, active, disabled, focus and selected all need values, and they are the tokens teams leave to components. That is how you get eleven hover greys.

The picker leaks primitives. If the design tool shows every primitive in the colour picker, designers will use them, because they are right there. The primitives should be hidden from the picker entirely and only semantic styles offered. Tooling enforces this far better than documentation does.

Nobody owns deprecation. Tokens accumulate. Without a route for removing one, the set only ever grows, and after two years the naming is archaeological.

What this looks like on a real product

On this site the same principle applies in miniature. `--ink`, `--paper`, `--rail` and `--hairline` are semantic; the `--grey-*` ramp behind them is primitive. Components consume the first set. That is why the whole site's separator weight is one line to change, and why the greys have not multiplied.

The site also carries a documented trap worth mentioning, because every system develops one: a legacy `--blue-*` set aliased onto a lime ramp, where `--blue-500` resolves to off-black. It is wrong and it is written down at the point of definition, which is the second-best outcome. The best outcome is not creating it.

What to do if you have a flat set already

You do not need to rebuild.

  1. Add the semantic layer on top of the primitives you have. Do not rename the primitives; that breaks everything at once for no benefit. 2. Pick the five decisions that appear everywhere. Body text, subordinate text, page background, raised surface, subtle border. That covers most of a product. 3. Migrate one component to consume only semantic tokens, and see what is missing. It will be states. 4. Hide primitives from the design tool's picker. 5. Add a lint rule that fails on a primitive referenced outside the token file. This is the step that makes the whole thing stick, and it is half a day.

The point

A token is not a variable for a colour. It is a place to record a decision once so that it can be changed once.

If the name tells you what the value is, you have written a palette. If the name tells you when to use it, you have written a system. The difference costs about a week up front and is the entire reason the effort pays back.

Frequently asked questions

What are design tokens?
Design tokens are named values that store design decisions such as colour, spacing, type size and radius, in a format both design tools and code can consume. A useful token system has at least two layers: primitives holding raw values, and semantic tokens naming a decision, such as text-muted or border-subtle, which is the layer components should reference.
What is the difference between primitive and semantic tokens?
A primitive token holds a raw value, such as grey-700 being a specific hex code, and says nothing about when to use it. A semantic token names a decision, such as text-muted, and points at a primitive. Components consume semantic tokens only, which means changing what subordinate text looks like is a one-line change rather than a search across the codebase.
How should I name design tokens?
Name the decision rather than the value. A good test is asking what would have to be true for the token's value to change; if the only answer is that the brand colour changed, the name is still describing a value. Prefer action-primary over color-brand-blue, and text-muted over grey-700, because the first of each pair survives a rebrand.
Do small teams need design tokens?
A two-layer set is worth it for almost any team maintaining a product over more than a few months, because the cost is a naming exercise and the payoff is that visual changes stay one-line. Component-level tokens are usually not worth it until several products share a system, at which point they become the mechanism for controlled divergence.

Sources

This thinking, applied

an audit that starts at the token layer

Share this

Keep reading

All posts