Skip to content
Resources

Guide

Semantic tokens vs. a list of hex codes

A palette tells you which colors. A theme tells you which color goes where, and proves the pairings are readable. The difference is most of the work.

7 min read

A palette is a list of colours. A theme is a set of decisions about which colour goes where. Handing a developer five hex codes and calling it a design system leaves the entire second half of the work undone — and the half that was left undone is the half that determines whether the result is readable.

The gap between a palette and a theme

Five swatches from a generator answer which colours. They do not answer which one is the page, which is a raised card, which is the border between them, which is body text versus muted text, which is the primary button — or, most consequentially, what colour the text on that button should be.

In practice those decisions get made anyway, one at a time, by whoever is writing the component at the time, under deadline. That is how a system ends up with three different greys for borders and a primary button whose label is white on one screen and dark on another.

Naming by role, not by appearance

The reason to name a token --color-danger rather than --color-red is not tidiness. It is that the appearance-named version becomes a lie the moment anything changes. Every system that has been alive for a few years contains a $blue that is green, because the brand moved and renaming the variable would have meant touching four hundred files.

Role names survive that. --color-primary is still accurate after a rebrand; --color-brand-blue-500 is a rename waiting to happen. The same logic makes dark mode possible at all — --color-background can be near-black in one theme and near-white in another, while --color-white obviously cannot.

A token should carry its own proof

This is the part usually missing. Most token sets state a value and nothing else: --color-on-primary: #ffffff. But white on the primary is an assumption, and on a yellow or light-green primary it is an unreadable one.

The alternative is to measure the pairing and ship the measurement with the token. Not “white, probably” but: the first candidate that cleared 4.5:1 against the derived primary, with the ratio recorded alongside. A token set that carries its own contrast proof is one nobody downstream has to re-audit — and the reason for each choice travels into the repository with it, as a $description in DTCG or a trailing comment in CSS.

That last detail matters more than it sounds. The goal is not to explain the decision inside our UI, where it is obvious and unnecessary. It is for the justification to survive the copy-paste into a codebase where, two years later, nobody remembers this tool was involved.

One more rule: never rotate hue

When a derived token has to be adjusted to clear a contrast threshold, only lightness and chroma move. A generator that reaches its target by turning your blue into a teal has technically passed and practically failed — and that single behaviour is why people stop trusting automated colour tools. The palette that comes back has to still look like the palette that went in.

Formats

The same derived theme serialises to whichever form your codebase reads, so the choice of format is not a choice about semantics:

  • CSS
  • SCSS
  • Tailwind v4
  • Tailwind v3
  • DTCG
  • JSON

One caution on the standard: in the stable Design Tokens Format Module, a colour $value is an object — { colorSpace, components, hex? } with sRGB components normalised to 0–1. The "$value": "#rrggbb" form that most tutorials still show is an older draft, and tools that validate against the current spec will reject it.

Derive tokens from your palette →