Most token systems fail in the same way. Someone exports the colours from Figma, names them blue-500 through blue-900, and six months later half the codebase uses blue-700 for text and the other half uses it for borders. Nothing is wrong, exactly, but nothing is reliable either.
Two layers, and only two
The fix is to separate what a colour *is* from what it's *for*.
The primitive layer is the raw palette. It's allowed to be boring and mechanical: --blue-500, --neutral-900. Nobody uses these directly in a component.
The semantic layer is the contract. --color-text-muted, --color-border-strong, --color-surface-raised. These are the only names a component is allowed to reference, and each one is a promise: *this will always be legible on the default surface*.
Once that split exists, theming becomes trivial. Dark mode isn't a set of overrides scattered through components — it's a redefinition of the semantic layer. The components don't change at all.
Name by role, never by appearance
The single most useful rule: if a token name would need to change when the design changes, it's the wrong name.
--color-accent survives a rebrand from orange to teal. --color-orange does not. --color-text-muted survives a decision to make secondary text darker. --color-grey-600 does not.
Make the contract enforceable
A convention that isn't checked is a suggestion. Two cheap enforcement mechanisms:
- Lint for raw colour values in component styles. A hex code or
rgb()in a component file is a bug by definition. - Don't export the primitives from your styling entry point. If
--blue-500isn't reachable, it can't be misused.
Cover state and hierarchy, not just colour
Teams usually get colour tokens right and then stop. But the same reasoning applies to everything the design repeats:
- Spacing as a scale, not arbitrary pixels.
- Elevation as named levels, so "raised" means one thing everywhere.
- Motion as named durations and easings — this one is routinely forgotten, and it's why animations across a large site feel subtly inconsistent.
- Radii, because "slightly rounded" is a decision someone will otherwise re-make weekly.
What you get
The payoff isn't fewer hex codes. It's that a designer can change a decision in one place and trust it propagates, and a developer can pick a token without needing to ask what it's for. That's the whole value: reduced coordination.