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.
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.
/* 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:
--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:
--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 16 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 93 role tokens, which sounds like a lot until you see what the layers are for:
- Foundations. The raw scales: the palette, spacing, radii, type sizes.
These are named after their values, and that is correct —
--gr-slate-500is a fact about a colour, not a decision about an interface. - 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. - 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.