The problem design tokens solve is drift. A designer changes a colour in Figma, tells a developer, the developer updates two of the five places it appears, and six months later nobody knows which value is correct.

Tokens fix this by making one definition the source everything else is generated from. Here's a pipeline that works, and the naming decisions that determine whether it survives contact with a redesign.


Name by role, not by appearance

This is the decision that matters most, and it's made early and cheaply or fixed late and expensively.

Bad: blue-500, gray-dark, font-16 Good: color-action-primary, color-text-secondary, font-size-body

Appearance-based names break the moment the appearance changes. When your brand blue becomes green, color-blue-500: #16a34a is a lie that will confuse everyone who reads it — and renaming it means touching every usage.

The practical approach is two layers:

Primitives — the raw palette, named by appearance because that's what they are:

json
{ "cyan": { "500": "#56c6e6" }, "ink": { "900": "#12100c" } }

Semantic tokens — named by role, referencing primitives:

json
{
  "color-background": "{ink.900}",
  "color-action-primary": "{cyan.500}",
  "color-text-primary": "{paper.100}"
}

Components only ever reference semantic tokens. A rebrand then means changing which primitive each semantic token points at — one file — rather than auditing every component.


The pipeline

Figma Variables are the practical source for design-owned values. They support modes, which map cleanly to light and dark themes, and they're exportable via the Figma API or a plugin.

Style Dictionary transforms one token definition into every output format you need.

code
tokens/
  primitives.json     raw palette, spacing, type scale
  semantic.json       role-based, references primitives
  
        ↓ style-dictionary build
        
build/
  variables.css       CSS custom properties
  tailwind.js         Tailwind theme extension
  tokens.ts           typed constants for JS

A minimal config:

javascript
// style-dictionary.config.js
export default {
  source: ["tokens/**/*.json"],
  platforms: {
    css: {
      transformGroup: "css",
      buildPath: "build/",
      files: [{ destination: "variables.css", format: "css/variables" }],
    },
    js: {
      transformGroup: "js",
      buildPath: "build/",
      files: [{ destination: "tokens.ts", format: "javascript/es6" }],
    },
  },
};

Producing:

css
:root {
  --color-background: #12100c;
  --color-action-primary: #56c6e6;
  --color-text-primary: #efe8db;
  --space-4: 1rem;
}

Wiring into Tailwind

Point Tailwind at the CSS custom properties rather than duplicating values. Then a token change propagates without a rebuild of the config.

javascript
// tailwind.config.ts
export default {
  theme: {
    extend: {
      colors: {
        background: "var(--color-background)",
        "action-primary": "var(--color-action-primary)",
        "text-primary": "var(--color-text-primary)",
      },
      spacing: {
        4: "var(--space-4)",
      },
    },
  },
};

This also makes theming trivial — override the custom properties under a selector and every Tailwind class using them follows:

css
:root { --color-background: #efe8db; --color-text-primary: #12100c; }

@media (prefers-color-scheme: dark) {
  :root { --color-background: #12100c; --color-text-primary: #efe8db; }
}

:root[data-theme="dark"] { --color-background: #12100c; --color-text-primary: #efe8db; }
:root[data-theme="light"] { --color-background: #efe8db; --color-text-primary: #12100c; }

The media query handles system preference; the data-theme attribute lets an explicit toggle override it in both directions.


Which tokens are worth defining

Colour — background layers, text levels, borders, action colours, semantic states. Keep it small.

Spacing — one scale, used everywhere. This prevents more visual inconsistency than colour tokens do, and gets neglected because it's less visible.

Typography — sizes, weights, line heights, letter spacing. Name by role: font-size-heading-1, not font-size-32.

Radii, borders, shadows — few values, consistently applied.

Motion — durations and easing curves. Almost always forgotten, and it's why animations feel inconsistent across a product built by several people.

Not worth tokenising: one-off values used in a single place. A token used once is indirection with no benefit.


The sync problem nobody solves cleanly

Be honest about this: fully automatic bidirectional sync between Figma and code does not really work. Every team that attempts it ends up with conflicts nobody wants to resolve.

What works in practice is picking a direction:

Design leads. Figma Variables are authoritative. A scheduled job or manual command pulls them via the API, regenerates tokens, and opens a pull request. Developers review and merge. Good when designers own visual decisions and iterate frequently.

Code leads. The token JSON in the repository is authoritative, and Figma is updated to match. Less elegant, more reliable, and appropriate when the product is engineering-driven.

Either is fine. What fails is having no declared direction, because then both drift and neither is trusted.

Whichever you pick, put token changes through code review. A colour change is a product change, and it deserves the same visibility as any other.


Worth adding to CI

  • Contrast checks. Assert that text and background token pairs meet WCAG AA. Cheap to automate, and it catches accessibility regressions at the source rather than in an audit.
  • Unused token detection. Tokens accumulate. Flag ones nothing references.
  • Hardcoded value linting. A rule rejecting raw hex values in component files is the single most effective enforcement mechanism available, because it makes the token the path of least resistance.

Starting realistically

You don't need the full pipeline on day one. A single variables.css with well-named semantic custom properties, referenced everywhere, delivers most of the value.

Add Style Dictionary when you need a second output format — a mobile app, an email template, a design tool. Add Figma sync when hand-copying values becomes the bottleneck.

Building the whole pipeline for one web app is over-engineering. Good naming is not.


Want a token system built?

We build design systems wired into code, with tokens as the source and enforcement in CI. Talk to us about graphic design and branding.