All articles
Design & UXAugust 11, 2026 6 min read

Design Tokens That Survive Contact With Engineering

Most design token systems die the moment they hit a real codebase. Here's how we structure tokens so they actually get used — and stay in sync between Figma, Tailwind, and native.

Every design system deck promises tokens as the single source of truth. Then six months later engineers are hard-coding #0F172A because nobody knows whether to use color.slate.900, color.text.primary, or --fg-strong. The tokens didn't fail — the layering did.

This is how we structure token systems at 72Technologies so they survive the handoff, the redesign, the dark mode retrofit, and the inevitable second brand.

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

The cleanest mental model we've landed on has three tiers: primitive, semantic, and component. Skip any of them and you'll feel it within a quarter.

  • Primitives are raw values. blue.500 = #3B82F6. No meaning, no intent. Just the palette.
  • Semantic tokens describe role. color.action.primary, color.surface.raised, space.gutter.md. This is the layer engineers should be reaching for 90% of the time.
  • Component tokens are local aliases. button.primary.background, card.border.default. They exist so a component can be restyled without ripping through the semantic layer.

The common mistake is to stop at two tiers — primitives plus components — and skip the semantic middle. It feels tidy in Figma, but the first time you build a new component you have no vocabulary for "the default surface" and you end up either reaching into primitives (bad) or duplicating component tokens (worse).

A concrete example

{
  "color": {
    "blue": { "500": { "$value": "#3B82F6" } },
    "action": {
      "primary": {
        "default":  { "$value": "{color.blue.500}" },
        "hover":    { "$value": "{color.blue.600}" },
        "pressed":  { "$value": "{color.blue.700}" }
      }
    }
  },
  "button": {
    "primary": {
      "background": { "$value": "{color.action.primary.default}" }
    }
  }
}

When the brand refreshes and blue.500 shifts, every semantic and component token downstream updates for free. When product decides the primary action should be green in one product line, you change color.action.primary and leave the palette alone.

Name tokens for intent, not appearance

This is the rule that catches the most heat from designers and pays back the most in engineering. color.gray.100 is not a token — it's a variable. color.surface.subtle is a token.

Intent-based names survive:

  • Dark mode (surface.subtle becomes darker; the name still fits)
  • Rebrands (the color changes; the role doesn't)
  • Accessibility fixes (bumping contrast doesn't require a rename)
  • White-labeling (a tenant can override action.primary without knowing your palette)

A good sniff test: if a name contains a color, size, or number, it belongs in the primitive layer only. Semantic tokens should read like a sentence describing what the thing does.

Figma variables → Tailwind, without the drift

Figma's variable system finally gave us a real bridge in 2024, and by 2026 most teams we work with are treating Figma variables as the authoring surface. The pipeline we run looks like this:

  1. Designers define primitives and semantic tokens as Figma variables, grouped by collection (Palette, Theme, Spacing, Type).
  2. A CI job exports variables to a W3C-format tokens JSON file via the Figma REST API or the Tokens Studio plugin.
  3. Style Dictionary transforms that JSON into platform-specific outputs: CSS custom properties, a Tailwind config, and native (Swift/Kotlin) constants.
  4. Tailwind consumes the generated file — never a hand-edited one.

The Tailwind piece is where teams get lazy. Don't paste hex values into tailwind.config.js. Point Tailwind at the CSS custom properties your build emits:

// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      colors: {
        surface: {
          base:    'rgb(var(--surface-base) / <alpha-value>)',
          raised:  'rgb(var(--surface-raised) / <alpha-value>)',
          subtle:  'rgb(var(--surface-subtle) / <alpha-value>)'
        },
        action: {
          primary:   'rgb(var(--action-primary) / <alpha-value>)',
          'primary-hover': 'rgb(var(--action-primary-hover) / <alpha-value>)'
        }
      }
    }
  }
};

Why rgb(var(...) / <alpha-value>) instead of just var(--action-primary)? Because it preserves Tailwind's opacity modifiers — bg-action-primary/20 still works. Store the raw channels (59 130 246) in the custom property, not the hex.

Theming falls out for free

With CSS custom properties driving Tailwind, dark mode and multi-brand theming become a scoping problem, not a build problem:

:root {
  --surface-base: 255 255 255;
  --action-primary: 59 130 246;
}

[data-theme='dark'] {
  --surface-base: 15 23 42;
  --action-primary: 96 165 250;
}

[data-brand='acme'] {
  --action-primary: 220 38 38;
}

No rebuild, no class explosion, no dark: prefix on every element. The component code just says bg-surface-base and does the right thing everywhere.

Contrast is a token concern, not a QA concern

One of the most common bugs we see: a semantic token pair like color.text.on-primary and color.action.primary drifts over time as the brand palette shifts, and suddenly your primary button is 3.8:1 instead of 4.5:1. Nobody notices until an audit.

Bake contrast checks into the token pipeline. It's a fifteen-line script:

import { getContrast } from 'polished';

const pairs = [
  ['action.primary', 'text.on-primary', 4.5],
  ['surface.base',   'text.primary',    4.5],
  ['surface.subtle', 'text.secondary',  4.5]
];

for (const [bg, fg, min] of pairs) {
  const ratio = getContrast(resolve(bg), resolve(fg));
  if (ratio < min) {
    throw new Error(`Contrast ${bg} vs ${fg} = ${ratio.toFixed(2)}, needs ${min}`);
  }
}

Run it in the same CI step that transforms tokens. A designer who breaks contrast gets a red build instead of a Slack message from an engineer three sprints later. In our experience this catches roughly one regression per major brand update — sometimes more when a junior designer is exploring.

Motion and spacing tokens deserve the same rigor

Color gets all the attention. Motion and spacing get hex-value-equivalents scattered through the codebase.

For motion, we define two primitives and let semantic tokens compose them:

  • Durations: motion.duration.fast (120ms), motion.duration.base (200ms), motion.duration.slow (320ms)
  • Easings: motion.easing.standard, motion.easing.emphasized, motion.easing.exit

Then semantic tokens like motion.hover, motion.modal-enter, motion.page-transition reference them. When a designer decides all hover states should feel snappier, you change one token, not forty components.

A useful ratio we've adopted: exit animations should run at ~75% of entry duration. Users tolerate slow reveals; they resent slow dismissals. Encoding that as motion.duration.exit = calc(var(--motion-duration-base) * 0.75) keeps the ratio consistent even when someone tweaks the base.

For spacing, use a scale (4px base, or a modular scale if you're feeling fancy) but expose semantic spacing tokens too: space.gutter, space.stack.tight, space.inset.card. A raw space.4 is fine for one-off layout; a card component should reference space.inset.card so all cards stay consistent when someone bumps the inset.

Where teams go wrong

A few patterns we've had to unwind on client projects:

  • Tokens that mirror the palette 1:1. If your semantic layer has color.blue, color.red, color.green — that's not semantic, that's a rename. Delete it.
  • Too many component tokens too early. Don't create card.header.title.font-weight before you have three cards. Component tokens should emerge from repetition, not speculation.
  • Tokens defined in code, mirrored in Figma manually. This lasts about two sprints. Pick one authoring surface and generate the other.
  • No versioning. Tokens are an API. Ship them as a versioned package your apps depend on, with a changelog. Breaking changes get a major bump.
  • Ignoring density. If you build both a marketing site and a data-dense admin panel, one spacing scale won't serve both. Consider a density dimension in your semantic layer.

What we'd do on a fresh project

Start with Figma variables and the W3C tokens format from day one — don't retrofit later. Wire Style Dictionary into CI in the first week, even if it only outputs three tokens. Emit CSS custom properties as your primary target and let Tailwind consume them via rgb(var(...)). Add the contrast check before you add your second color pair. Resist creating component tokens until you have real duplication to collapse.

If you're staring at an existing system that's already drifting, the highest-leverage move is usually inserting the semantic layer between your primitives and your components — even if you have to do it one surface at a time. Tokens are worth doing properly, or not at all. Half-built token systems are worse than none, because they give everyone permission to stop thinking.

If you want a second pair of eyes on your token architecture or the Figma-to-code pipeline behind it, that's the kind of work our design and engineering teams do together — and we've written more on adjacent patterns over on the blog.

#Design Systems#Tokens#Tailwind#Figma#Accessibility

Want a team like ours?

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

Start a project