Skip to main content
design systems

Design Tokens Are Naming Decisions Before They Are JSON

The format is solved. The naming is not. Every token decision is an opinion about how your design and engineering teams should communicate.


6 min read

The W3C Design Token Community Group format (DTCG) is settled. The JSON structure, the $value and $type fields, the composite token spec — these are defined, tooled, and supported by Tokens Studio, Style Dictionary, and most major design tools. The format is not the problem. The problem is what you put in it. Every token name is an assertion about how your design team and engineering team should communicate about visual decisions, and most naming schemes I encounter assert the wrong things.


Three Layers, Each With a Different Job

A workable token architecture has three tiers:

Primitive tokens are the raw values. No semantics, no context. Every color in your palette, every spacing step, every font size. These exist so the rest of the system has something to reference.

{
  "color": {
    "blue": {
      "500": { "$value": "#3b82f6", "$type": "color" },
      "600": { "$value": "#2563eb", "$type": "color" }
    },
    "neutral": {
      "900": { "$value": "#111827", "$type": "color" }
    }
  }
}

Semantic tokens are aliases that carry meaning. They reference primitives and describe what a value is for, not what it looks like. This is where theming happens.

{
  "color": {
    "action": {
      "primary": { "$value": "{color.blue.600}", "$type": "color" },
      "primary-hover": { "$value": "{color.blue.700}", "$type": "color" }
    },
    "text": {
      "default": { "$value": "{color.neutral.900}", "$type": "color" }
    }
  }
}

Component tokens are semantic tokens scoped to a specific component. They exist when a component needs to be themed independently from the broader semantic layer.

{
  "button": {
    "background": { "$value": "{color.action.primary}", "$type": "color" },
    "background-hover": { "$value": "{color.action.primary-hover}", "$type": "color" }
  }
}

Each layer has a different consumer. Primitive tokens are consumed by semantic tokens. Semantic tokens are consumed by component tokens and directly by layout/spacing/typography decisions. Component tokens are consumed by component implementations.

The Tension: Too Specific vs. Too General

The failure mode on one end is tokens like color.button.primary.background.hover.default. This is a token for one exact state of one exact component. If you add a secondary button, you add a new tree. If you rename the hover state to "focused+hovered" to match WCAG guidance, you rename tokens across every component. The naming is precise but brittle.

The failure mode on the other end is tokens like color.primary. This means "primary color" — which is correct for brand use, for the action color in buttons, for active state indicators, and for link text. Now you can't update button hover behavior without potentially affecting link colors. Everything shares the token and nothing can be controlled independently.

The right position is semantic tokens that name the intent at the right altitude. color.action.primary is at the right altitude for button backgrounds and interactive element fills. color.text.on-action is at the right altitude for text that sits on top of those elements. Neither is so specific that it locks a single component, nor so general that it can't be changed without side effects.

A Bad Naming Scheme and Why It Fails

Here's a scheme I've inherited twice:

color-primary
color-primary-dark
color-primary-darker
color-primary-light
color-primary-lighter
color-secondary
color-secondary-dark
...

The problems compound quickly. "Dark" means darker than primary — but how much darker? That depends on the design tool and whoever added it. "Secondary dark" is secondary's dark variant — but is it used for secondary button hover, or secondary text on a light background, or the secondary color in charts? The names describe visual relationships (-dark, -light) rather than intended usage.

When a designer updates the color palette, they update color-primary and expect button backgrounds to change. But color-primary is also used for alert icons, chart fill, active sidebar items, and focus rings — because it's the only blue in the token set, so engineers reached for it everywhere. The semantic layer doesn't exist, so primitives are used directly in components, and a palette change breaks seventeen things.

A Better Scheme

{
  "color": {
    "blue": {
      "400": { "$value": "#60a5fa", "$type": "color" },
      "500": { "$value": "#3b82f6", "$type": "color" },
      "600": { "$value": "#2563eb", "$type": "color" },
      "700": { "$value": "#1d4ed8", "$type": "color" }
    }
  },
  "color-action": {
    "default": { "$value": "{color.blue.600}", "$type": "color" },
    "hover": { "$value": "{color.blue.700}", "$type": "color" },
    "subtle": { "$value": "{color.blue.400}", "$type": "color" }
  },
  "color-feedback": {
    "info": { "$value": "{color.blue.500}", "$type": "color" }
  }
}

color-action and color-feedback can both reference blue primitives and diverge independently. If the design language shifts so that informational indicators use teal instead of blue, color-feedback.info changes without touching color-action. The semantic names describe purpose, not appearance.

Why Naming Reveals Team Structure

The naming scheme you adopt is a communication contract. "Who owns the semantic layer?" is an organizational question before it's a technical one. In some teams, designers name the semantic tokens and engineers consume them without input. In others, engineers have been burned by vague names and have started writing their own. In the worst cases, there are two parallel token sets — one in Figma, one in the codebase — with loose correspondence.

The sign that the naming scheme is working: designers can update a semantic token's value in Figma and describe the intended scope of change ("this should affect all interactive elements, not info states"). Engineers implement the change by updating one value and the component tests confirm the right things changed. No one has to grep for every usage of a hex value.

The sign it's not working: when you change a token value, you open Storybook and check every component manually to see what broke. The token names didn't communicate scope, so the scope is unknown.

My Preferred Approach

I use three files: primitives.tokens.json, semantic.tokens.json, and component.tokens.json. Primitives are named by scale (colors by palette+step, spacing by multiplier, font sizes by scale name). Semantic tokens are named by role: color-text-*, color-surface-*, color-border-*, color-action-*, color-feedback-*. Component tokens only exist when a component needs independent theming — form inputs, data visualization, code blocks. Everything else consumes semantic tokens directly.

Brand tokens — the values that change per-theme — live only in the primitive and semantic layers. Component tokens never contain brand values; they always alias semantic tokens. This means theming works by swapping semantic token values, and component implementations don't need to know they're inside a themed context.

The format is solved. These are the names.