# Tokens are roles, not colors

> Why a token named for its role outlives one named for its value, what breaks the day the brand colour changes, and where the rule stops.

- Page: https://granularity.tech/blog/tokens-are-roles
- Last reviewed: 2026-09-03

Every design system arrives at the same fork, usually in week two. You have a
palette. Do you call the token `--blue-500`, or do you call it `--primary`?

The answer is not a matter of taste, and the cost of the wrong branch is not
paid on the day you choose it. It is paid the day someone asks for a dark theme,
or the brand turns green, or a designer needs to know which of your eleven greys
is the one for borders.

## The failure is renaming, not choosing

Name a token after its value and you have written the value down twice: once in
the name, once in the declaration. They agree on day one. They stop agreeing the
first time the value changes.

```css
/* Day one: honest. */
--blue-500: #4f46e5;

/* Month four: the brand is green now. */
--blue-500: #16a34a;
```

Nobody renames it. Renaming means touching every call site, and the call sites
are exactly the places you did not want to touch — that is why you made a token.
So the codebase now says `blue` and means `green`, and every new person reads the
name and trusts it.

A role does not have this failure mode, because a role is not a claim about a
value:

```css
--gr-primary: #16a34a;
```

Nothing to rename. The token said "the main action colour" all along, and the
main action colour changed, which is the one thing tokens exist to allow.

## Roles come in pairs, and that is the actual point

The strongest argument for roles is not naming, it is that roles let you state
**relationships** the palette cannot.

A colour token alone does not tell you what goes on top of it. A role pair does:

```css
--gr-primary: #4f46e5;
--gr-primary-fg: #ffffff;
```

Every surface has a foreground, and the pair is a contract: *if you paint with
this, write with that.* You can check a contract. This portal checks 18 token pairs in both
themes on every build, and the check runs the library's own
contrast function rather than a second copy of the formula — two implementations
of one rule diverge, and the day they diverge nobody notices.

With `--blue-500` there is no pair to check. There is a blue, and a hope.

## The three-layer split

In practice one flat list does not survive either. The tokens here are three
groups doing three different jobs — 104 foundation tokens and 94 role tokens,
which sounds like a lot until you see what the layers are for:

1. **Foundations.** The raw scales: the palette, spacing, radii, type sizes.
   These *are* named after their values, and that is correct — `--gr-slate-500`
   is a fact about a colour, not a decision about an interface.
2. **Roles.** What the interface is made of: `--gr-bg`, `--gr-fg`, `--gr-primary`,
   `--gr-danger`, each with its foreground. Roles reference foundations. The
   application only ever sees this layer.
3. **Derived.** States computed from roles: hover, active, disabled, the focus
   ring. Derived by formula, not by eye, so that a new brand colour produces a
   correct hover without anyone picking one.

The layering is what makes "rename nothing" true. A rebrand rewrites layer one
and part of layer two. Layer three follows by construction, and the components —
which only ever named roles — do not change at all.

## Where the rule stops

Two honest limits, because a rule stated without them is a slogan.

**Roles do not survive being too few.** If everything is `--gr-primary`, you
have not built a role system, you have built one variable. The tell is a
component that has to override the token locally to look right; that override is
a role you failed to name. Those role tokens sit in 8 groups, and the number went
up, not down, as the components got more honest.

**Per-component tokens are not a failure of the role layer.** A dropdown's
maximum panel width is not a role — nothing else in the interface has an opinion
about it. Forcing it into the global layer would make the global layer a junk
drawer. The rule is narrower than it looks: *global tokens are roles.* Local
tokens are allowed to be specific, and they should be named after the component
that owns them.

## The test

If you want to know whether your tokens are roles, do not read them. Do this
instead: **rename the brand colour in your head and count the lies.**

Take `--blue-500` and imagine the brand turning green. Every occurrence of the
word "blue" in your codebase is now a lie you have chosen to live with. Take
`--gr-primary` and do the same. There is nothing to count.

That is the whole argument, and it takes ten seconds to run on any design system,
including the one you already have.
