All articles
Design & UXSeptember 7, 2026 6 min read

Design Tokens That Survive Contact With Engineering: A Naming Convention That Scales

Most design token systems die at the second theme. Here's a three-tier naming convention that survives dark mode, brand refreshes, and the inevitable Tailwind migration.

Design Tokens That Survive Contact With Engineering: A Naming Convention That Scales

Every design system we've audited in the last two years has the same failure mode: the tokens looked clean on day one, and by month eighteen the codebase has color-blue-500, color-primary, brand-blue, and --btn-bg-default all pointing at slightly different hex values. Nobody knows which one to use. That's not a tooling problem. It's a naming problem, and it compounds.

This is the token architecture we now ship by default, why each layer exists, and the specific mistakes that will bite you if you skip one.

The three-tier model, and why two tiers isn't enough

Most teams start with two layers: raw values and "semantic" aliases. blue-500 becomes primary, and everyone claps. Then dark mode arrives, or a sub-brand, or a marketing site that wants warmer greys, and the semantic layer starts leaking implementation details. primary is suddenly overloaded — it's a button colour, a link colour, and a focus ring, and those three things want to diverge.

The fix is three tiers, each with a strict job:

  1. Primitive tokens — raw values. No meaning, no context. blue-500, space-4, radius-md.
  2. Semantic tokens — intent, decoupled from component. text-accent, surface-raised, border-danger.
  3. Component tokens — bindings for a specific UI element. button-primary-bg, card-border, input-focus-ring.

Each layer only references the one above it. Component tokens never reference primitives directly. Semantic tokens never reference other semantic tokens. Break either rule and you've reintroduced the problem the layers exist to solve.

Why the discipline matters

When a designer says "make the danger button slightly more orange in the marketing theme," you want to change one component token, not hunt through 40 places where red-600 appears. When a rebrand shifts the entire palette, you swap primitives and everything downstream updates. When you add a high-contrast accessibility theme, you override semantics without touching components.

Each tier absorbs a different kind of change. That's the whole point.

What each tier actually looks like

Here's the shape we use in a typical project, expressed as JSON because it's what most token pipelines (Style Dictionary, Tokens Studio, the W3C DTCG format) speak natively.

{
  "color": {
    "primitive": {
      "blue": { "500": { "$value": "#2563eb", "$type": "color" } },
      "slate": { "900": { "$value": "#0f172a", "$type": "color" } }
    },
    "semantic": {
      "text": {
        "default": { "$value": "{color.primitive.slate.900}" },
        "accent":  { "$value": "{color.primitive.blue.500}" }
      },
      "surface": {
        "base":    { "$value": "{color.primitive.white}" },
        "raised":  { "$value": "{color.primitive.slate.50}" }
      }
    },
    "component": {
      "button": {
        "primary": {
          "bg":   { "$value": "{color.semantic.surface.accent}" },
          "text": { "$value": "{color.semantic.text.on-accent}" }
        }
      }
    }
  }
}

Note what's happening. button.primary.bg doesn't know it's blue. It knows it wants the accent surface. If marketing decides the accent surface should be teal for Q3, the button follows automatically. If accessibility audit finds the button needs a darker shade only in that specific context, you override at the component tier without disturbing anything else.

Naming rules that prevent the mess

A few conventions we enforce in code review:

  • Primitives are descriptive, never prescriptive. blue-500, not primary-blue. The moment a primitive has intent baked in, it can't be reused for a different intent.
  • Semantics describe the job, not the appearance. text-danger, not text-red. surface-raised, not surface-light-grey.
  • Component tokens include the component name and the state. button-primary-bg-hover, not btn-hover-1. Verbose beats ambiguous.
  • No colour words below the primitive tier. If you see red or blue in a semantic or component token, it's a bug.

The Figma-to-Tailwind path without the pain

The workflow we run for most product teams looks like this:

  1. Designers manage primitives and semantics in Tokens Studio inside Figma.
  2. Tokens sync to a Git repo as DTCG-format JSON.
  3. Style Dictionary transforms them into a Tailwind config and a CSS variables file.
  4. Component tokens live in code, not Figma, because they're implementation details.

That last point is contentious and worth defending. Designers do not need to name every button state token. They need to control the palette and the semantic intent. Component bindings are an engineering concern — they change when the component API changes, and forcing them through Figma slows both sides down.

The generated Tailwind config ends up looking like this:

// tailwind.config.js (generated)
module.exports = {
  theme: {
    colors: {
      // semantic tokens exposed as utilities
      'text-default': 'var(--color-text-default)',
      'text-accent':  'var(--color-text-accent)',
      'surface-base': 'var(--color-surface-base)',
      'surface-raised': 'var(--color-surface-raised)',
      // ...
    }
  }
}

Components then use semantic utilities directly for one-off elements, and component tokens (via CSS variables) for anything reusable:

<button className="bg-[var(--button-primary-bg)] text-[var(--button-primary-text)] hover:bg-[var(--button-primary-bg-hover)]">
  Save changes
</button>

We deliberately don't expose primitives as Tailwind utilities. If bg-blue-500 is available, someone will use it, and the whole system leaks. Restrict the surface area and the discipline enforces itself.

Theming without regret

Dark mode is where two-tier systems collapse. With three tiers, it's boring — which is what you want.

A theme is a set of semantic overrides. Primitives don't change. Component tokens don't change. You swap the mapping between semantics and primitives:

:root {
  --color-surface-base: var(--color-primitive-white);
  --color-text-default: var(--color-primitive-slate-900);
}

[data-theme="dark"] {
  --color-surface-base: var(--color-primitive-slate-950);
  --color-text-default: var(--color-primitive-slate-100);
}

[data-theme="high-contrast"] {
  --color-surface-base: #000;
  --color-text-default: #fff;
}

Because components bind to semantics, they inherit the theme automatically. No component-level dark: variants scattered across the codebase. No forgetting to update the one card that renders in a modal. Add a theme, ship it, done.

The contrast trap

One warning. When you decouple semantics from primitives, it's easy to accidentally pair a foreground and background that don't meet WCAG contrast in one theme but do in another. Bake a contrast check into your token build. Style Dictionary supports custom transforms; a 20-line script that fails the build when text-default on surface-base drops below 4.5:1 has saved us more than one embarrassing shipped bug.

Migration: how to introduce this into an existing codebase

Don't do a big-bang rename. It will stall and you'll ship half a system, which is worse than either extreme.

The order that has worked for us:

  1. Freeze the primitives. Audit every hex in the codebase, collapse near-duplicates, name them. Don't touch usage yet.
  2. Introduce semantic tokens alongside existing ones. Both work. New code uses semantics, old code keeps working.
  3. Migrate component by component, not file by file. Ship a PR per component with visual regression snapshots.
  4. Lint the old tokens as deprecated once coverage is above ~70%. Fail the build only when it hits 95%.
  5. Add component tokens last, only where a component has genuinely divergent needs. Don't create component tokens speculatively — you'll end up with 400 tokens nobody uses.

This is dull, incremental work. It's also the only version that finishes.

Where we'd start

If you're staring at a design system that's starting to fray, don't rewrite it. Spend a day auditing your current tokens and sorting them into the three tiers on paper. You'll immediately see which ones are miscategorised — the "semantic" token called blue-primary, the "primitive" called brand, the component style that references a raw hex. Fix the naming first, in one file, before you touch a single component. The refactor gets easier from there, and the next theme you ship won't require a war room.

If you'd rather have someone else run that audit, our design systems work is largely this kind of unglamorous plumbing — and it's usually the highest-leverage week a product team spends all year.

#Design Systems#Design Tokens#Tailwind#Figma#Theming

Want a team like ours?

72Technologies builds production software for the kind of teams who actually read this blog.

Start a project