Design token naming: the three-tier system, and where it breaks

Everyone converges on the same three tiers and gets different results. The naming convention is not the hard part. The hard part is deciding what deserves a semantic name at all, and almost every failed token system failed by naming too much.

Updated July 27, 2026

The three tiers, and what each one is actually for

The convention is close to universal, which makes it easy to adopt the shape without the reasoning. Each tier answers a different question, and knowing which question keeps you from putting things in the wrong place.

AnswersExampleUsed by
PrimitiveWhat values exist in this design?blue-600, space-4, radius-mdThe semantic tier only. Never product code
SemanticWhat is this value for?--color-danger, --text-muted, --surface-raisedProduct code. This is the layer people write against
ComponentWhere does one component legitimately differ?--button-primary-bg, --tooltip-surfaceThat component only, and rarely
The three tiers and their jobs.

The rule that makes this work, and the one most often broken: nothing outside the semantic tier may reference a primitive. The moment a component uses blue-600 directly, the primitive tier has become a public API and you can no longer change a value without auditing the whole codebase — which was the entire reason for having tiers.

:root {
  /* Tier 1: primitives. Values only. Nobody uses these directly. */
  --blue-600: #2563eb;
  --red-600:  #dc2626;
  --gray-500: #6b7280;

  /* Tier 2: semantic. Roles. This is the public API. */
  --color-primary:   var(--blue-600);
  --color-danger:    var(--red-600);
  --text-muted:      var(--gray-500);

  /* Tier 3: component. Only where a component truly deviates. */
  --button-danger-bg: var(--color-danger);
}

/* ✅ product code */
.alert { color: var(--color-danger); }

/* ❌ reaches past the semantic layer */
.alert { color: var(--red-600); }

The naming pattern

Read left to right, general to specific: category, role, variant, state. Not every token needs all four, and the ones that do are usually interactive.

CategoryRoleVariantState
color-text-primarycolortextprimary
color-surface-raisedcolorsurfaceraised
color-action-primary-hovercoloractionprimaryhover
space-inset-lgspaceinsetlg
border-subtlebordersubtle
The pattern applied.

Consistency matters far more than which pattern you pick. A codebase where half the tokens read text-color-muted and half read color-text-muted costs everyone a lookup every time, forever. Pick one order, write it down, and enforce it in review.

One practical advantage of general-to-specific ordering: your tokens sort alphabetically into meaningful groups. All the color-surface-* tokens sit together in an editor's autocomplete, which turns the naming convention into a discovery mechanism.

Here is the pattern applied, rather than described. Read the role names, not the colours — the question each one answers is "what is this for", and a name that only answers "what colour is it" has failed the tier it sits in.

Preview unavailable here. Browse complete kits in the kit gallery.

The test that catches a bad name

One question resolves most naming arguments in seconds:

If the design changed, would this name still be true?

Run it against real cases and the answers are unambiguous:

Survives?Why
--color-brand-blueNoRebrand to green and the name is a lie that nobody dares fix
--color-primaryYesThe role does not change when the hue does
--text-small-grayNoTwo appearance facts, both of which will move
--text-mutedYesNames the intent — de-emphasised text
--shadow-cardNoEncodes the mechanism. Switch to hairline elevation and it is wrong
--elevation-raisedYesNames the effect. It can be a shadow, a border, or a surface step
Names that survive a redesign, and names that do not.

The --shadow-card case is the subtle one, and worth dwelling on. Naming a token after its implementation locks the implementation in. Once every card in the codebase says box-shadow: var(--shadow-card), moving to border-based elevation is a refactor rather than a token change — which is exactly the coupling tokens were supposed to remove.

Dark mode is where appearance-based names die publicly. --gray-100 as "the light background" is correct in one theme and inverted in the other, so you end up with --gray-100 holding a near-black value and every reader confused. --surface-base is true in both.

Why the component tier keeps growing

Most token systems that fail, fail here. The component tier starts small and legitimate, then absorbs everything, and eventually you have four hundred tokens, half of which have one consumer.

The mechanism is always the same. Someone needs a button background that is not quite --color-primary. Adding --button-primary-bg takes thirty seconds and adding it to the semantic tier requires a conversation. So the component token goes in, and the next person does the same, and the semantic tier stops being the source of truth without anyone deciding that.

The real problem
Several components need the same off-semantic valueA missing semantic token. Name the role and promote it
One component needs a genuinely unique valueLegitimate. This is what the tier is for. It should be rare
A whole surface needs different valuesA missing theme or scope, not component tokens. Look at what Encore did with layers
Nobody can tell which semantic token to useThe semantic tier is under-specified or badly named
What a growing component tier is actually telling you.

A useful audit: count component-tier tokens with exactly one consumer. If that number is large, the tier is being used as an escape hatch, and the fix is upstream.

Five mistakes, and what each one costs

These recur across almost every token system that stops being used. Each has a cheap fix if caught early and an expensive one if not.

  1. 1

    Numbered scales with no anchor

    color-1 through color-12 tells nobody anything, and the numbers acquire meaning only through folklore. Numbered scales are fine in the primitive tier, where the number tracks a real dimension like lightness. They are never acceptable in the semantic tier, because the whole point of that tier is to say what something is for.

  2. 2

    Encoding the theme in the name

    --light-bg and --dark-bg as separate tokens means every component references both and branches. One name, two values, switched by theme — that is what the token layer is for. If you find if (theme === 'dark') in component code, the tokens are named wrong.

    /* ❌ */  --light-bg: #fff;  --dark-bg: #0a0a0a;
    /* ✅ */  --surface-base: #fff;
             [data-theme="dark"] { --surface-base: #0a0a0a; }
  3. 3

    Sizes named after where they are used today

    --text-hero is fine until the hero size shows up in a pricing table. --text-4xl or --font-scale-7 names the step, not the site, and survives being reused. Name by position in a scale, not by first customer.

  4. 4

    Status colours doubling as brand colours

    The most common way a system becomes unable to show state. If the brand green and the success green are the same token, you cannot restyle the brand without changing what success looks like, and you cannot tell a healthy row from a branded one. Keep the status set reserved and say so in a comment.

  5. 5

    Abbreviations that only one person expands

    --clr-bg-scndry saves eleven characters and costs every reader a decoding step forever, including a model that has to guess whether scndry is secondary or a typo. Autocomplete makes length nearly free. Write the words.

The second one is worth auditing today. Grep your components for theme conditionals — every one you find is a token that should have been one name with two values, and each is a place where dark mode will silently diverge from light.

Names that survive leaving your codebase

Tokens increasingly have to travel: from a design tool to CSS, into a Tailwind theme, into a native platform, into a shadcn registry item, into a DESIGN.md an agent reads. That puts a constraint on names most teams do not think about until the first export breaks.

Portable?Why
Dots or slashes as separatorsRiskycolor.text.muted is natural in JSON and illegal in a CSS custom property. Hyphens survive everywhere
Uppercase or mixed caseRiskySome targets normalise case, some do not. Lowercase throughout removes the question
Deep nestingRiskycolor.semantic.text.emphasis.high flattens into something unreadable and nobody types it twice
Flat lowercase hyphenatedYes--color-text-muted works as a CSS variable, a JSON key, a Tailwind key and a plain word in prose
What survives a format change, and what does not.

The practical rule: pick names that are simultaneously a valid CSS custom property, a valid JSON key and a readable English phrase. Flat, lowercase, hyphenated satisfies all three, and the DTCG/W3C token format's nested groups can be generated from a flat set far more easily than the reverse.

One more portability test worth applying: can you say the token name out loud in a meeting without spelling it? If two people cannot agree on how to pronounce a token, they will not use it consistently in writing either.

Making design and code use the same names

The highest-leverage naming decision is not the pattern. It is whether the names in your design tool are identical to the names in code.

Salesforce's SLDS 2 is the clearest published example: its Figma library uses the same semantic hook names as the CSS — their own examples are radius-border-4 and font-scale-4 — so designs map one-to-one to live code. Their stated goal is a shared vocabulary bridging design and development.

The practical effect is that a whole class of handoff bug stops existing. When a designer says font-scale-4 and a developer types font-scale-4, there is no translation step and therefore nowhere for meaning to leak. Compare that with a Figma style called "Heading / Large" mapping to a CSS variable called --text-2xl, where every handoff is a lookup and every lookup is a chance to guess wrong.

If you can only make one change to your token system, make the names match across tools. It costs a rename and removes a permanent tax.

Naming when an agent is the consumer

Token naming used to be a human ergonomics question — autocomplete, readability, onboarding. Increasingly the heaviest reader of your token file is a coding agent, and that changes which failures matter.

A human who cannot tell which of two greys to use will ask, or pick one and be told in review. A model will pick one, silently, in every file it touches, and the inconsistency arrives faster than review can catch it. Ambiguity in the semantic tier is a much more expensive defect than it used to be.

We sampled 299 DESIGN.md files published for agents to read and found that 86% specify colours as raw hex values with no semantic role attached at all. Not badly-named tokens — no tokens. A model given #6b7280 and told it is part of the palette will use it wherever a mid-grey is plausible: body text, borders, icons, placeholders, disabled states. Five different jobs, one value, no way to change any of them independently later.

What the agent does
Gray: #6b7280Uses it for body text, borders, icons, placeholders and disabled states alike
--text-muted: #6b7280 — secondary and supporting text only. Borders use --border-subtle. Disabled uses --text-disabled.Uses it for supporting text. The other jobs have their own names, so they can diverge later
The same grey, described two ways.

The second version costs three extra lines and buys the ability to darken your borders without darkening your captions — which is a change you will want, and which the first version makes impossible without a codebase-wide audit.

Identity Forge design kits ship 28 semantic colour roles across light and dark, plus type, spacing and elevation, serialised into a DESIGN.md an agent reads before writing anything. Browse the kits, or read semantic colour tokens explained.

A starting set

If you are naming a system from scratch, this is a defensible minimum. It is small on purpose — a system nobody can hold in their head gets ignored.

/* Surfaces — what things sit on */
--surface-base        /* the page */
--surface-raised      /* cards, panels */
--surface-overlay     /* modals, popovers */
--surface-sunken      /* wells, inset areas */

/* Text — by emphasis, never by colour */
--text-primary
--text-secondary
--text-muted
--text-disabled
--text-on-accent      /* text sitting on the accent colour */

/* Borders — by weight of presence */
--border-subtle
--border-strong
--border-focus

/* Action — the interactive colour and its states */
--action-primary
--action-primary-hover
--action-primary-active

/* Status — reserved. Nothing decorative uses these. */
--status-success
--status-warning
--status-danger
--status-info

Two notes on that set. Text tokens are named by emphasis rather than colour, so they stay true in dark mode. Status tokens carry a comment reserving them, because the most common way a status system breaks is a designer using the success green as a decorative accent and then nobody being able to tell a healthy row from a branded one.

What is the best design token naming convention?

Three tiers — primitive, semantic, component — with names read general to specific: category, role, variant, state. The specific ordering matters far less than applying one consistently, because the real cost of inconsistency is a lookup on every single use.

What is the difference between primitive and semantic tokens?

A primitive names a value (blue-600) and says nothing about where it belongs. A semantic token names a job (--color-primary) and points at a primitive. Product code should only ever use semantic tokens, so that changing a value is a one-line edit rather than a codebase audit.

Should I name tokens after colours?

Only in the primitive tier, where naming the value is the point. In the semantic tier, never: --color-brand-blue becomes a lie the moment you rebrand, and nobody renames it because too much depends on it. Name the role and let the value move underneath.

How many design tokens should a system have?

Fewer than you think. A semantic layer of roughly 25 to 40 colour roles plus type, spacing and elevation scales covers most products. If the count is climbing past a hundred, check how many have exactly one consumer — that number tells you whether you have a system or a list.

Do token names matter more now that AI writes the code?

Yes, because the failure mode changed. A human unsure which grey to use asks or gets corrected in review. A model picks one silently in every file it touches, so ambiguity spreads faster than review catches it. Unambiguous role names with stated purposes are the fix.