Shipping Design Tokens From Figma to Tailwind Without Losing Your Mind
A practical pipeline for syncing Figma variables to Tailwind v4 tokens, with the trade-offs we hit on real projects and the bits we'd skip next time.
Every design system project hits the same wall around month two: the designers are happily renaming variables in Figma, and the engineers are quietly hardcoding hex values because the sync script broke again. The fix isn't a fancier plugin — it's a boring, well-defined pipeline. Here's the one we keep coming back to for Tailwind v4 projects in 2026.
Why the naive approach falls apart
The first instinct is usually: export Figma variables to JSON, paste them into tailwind.config, done. This works for about a week. Then one of three things happens:
- A designer adds a new semantic token (
surface-raised) but engineers don't know it exists until a PR review. - Dark mode gets added, and suddenly every color needs two values, but the export format wasn't designed for modes.
- Someone renames
brand-500toprimary-500in Figma, and a hundred class names break silently in production because Tailwind just generates whatever you ask it to.
The real problem isn't the tooling — it's that there's no single source of truth and no contract between the two sides. A pipeline fixes that by making Figma the source, a token file the contract, and Tailwind the consumer.
The three-layer token model
Before touching any tooling, agree on the layers. We use three, borrowed loosely from the Design Tokens Community Group spec:
- Primitive tokens — raw values.
blue-500: #2563eb,space-4: 16px. Designers can change these, but engineers never reference them directly. - Semantic tokens — intent.
color-surface-default,color-text-muted,radius-control. These reference primitives and are what components actually use. - Component tokens — optional, but useful for complex components.
button-primary-bg,card-shadow-hover.
Why this matters for the pipeline
When a designer swaps the brand from blue to violet, only primitives change. Semantic tokens keep their names, so no engineering work is required. When a designer adds a new variant of a card, only component tokens change, and the blast radius is one component. Without layering, every color change is a refactor.
In Figma, this maps cleanly onto Variable Collections: one collection per layer, with modes (Light/Dark, or Desktop/Mobile) on the semantic and component layers only. Primitives stay mode-less.
The pipeline, end to end
Here's the flow we run on most projects:
Figma Variables
↓ (Variables REST API or Tokens Studio export)
tokens.json (W3C DTCG format)
↓ (Style Dictionary v4)
tokens.css (CSS custom properties)
↓ (Tailwind v4 @theme directive)
Utility classes generated at build time
Four stages, each with a clear contract. Let's walk through each.
Stage 1: Getting tokens out of Figma
Two viable options in 2026:
- Figma's Variables REST API. Free with any paid Figma plan that supports variables. You write the export script. More control, more code to maintain.
- Tokens Studio for Figma. Plugin-based, handles modes and references well, can push directly to a Git repo. Less code, but you're tied to its data format and its conventions.
For teams with one or two designers and a single product, Tokens Studio is usually the right call. For larger orgs with strict naming rules or unusual mode setups, the REST API wins because you can enforce conventions in code.
If you go the API route, the script that matters looks roughly like this:
import { writeFileSync } from 'node:fs';
const FILE_KEY = process.env.FIGMA_FILE_KEY!;
const TOKEN = process.env.FIGMA_TOKEN!;
const res = await fetch(
`https://api.figma.com/v1/files/${FILE_KEY}/variables/local`,
{ headers: { 'X-Figma-Token': TOKEN } }
);
const { meta } = await res.json();
const tokens = transformToDTCG(meta.variables, meta.variableCollections);
writeFileSync('tokens/tokens.json', JSON.stringify(tokens, null, 2));
The transformToDTCG function is where the work lives: walking Figma's flat variable list, resolving aliases, grouping by collection, and emitting the W3C format. Budget a day to write it properly and add tests.
Stage 2: Style Dictionary as the translator
Style Dictionary v4 added native DTCG support, which is the main reason we still reach for it over newer alternatives. It takes the token JSON and outputs whatever format you need — CSS variables, JS objects, iOS XML, Android resources. For Tailwind, we only care about CSS.
A minimal config:
// style-dictionary.config.js
export default {
source: ['tokens/tokens.json'],
preprocessors: ['tokens-studio'],
platforms: {
css: {
transformGroup: 'css',
buildPath: 'src/styles/',
files: [{
destination: 'tokens.css',
format: 'css/variables',
options: { selector: ':root' }
}]
},
cssDark: {
transformGroup: 'css',
buildPath: 'src/styles/',
files: [{
destination: 'tokens.dark.css',
format: 'css/variables',
options: { selector: '.dark' },
filter: (t) => t.path.includes('dark')
}]
}
}
};
The output is unglamorous and exactly what you want:
:root {
--color-surface-default: #ffffff;
--color-text-muted: #64748b;
--radius-control: 0.5rem;
--space-4: 1rem;
}
Stage 3: Tailwind v4's @theme directive
This is where Tailwind v4 changes everything compared to v3. Instead of a JavaScript config object, you point Tailwind at CSS variables using @theme:
@import 'tailwindcss';
@import './tokens.css';
@theme {
--color-surface: var(--color-surface-default);
--color-muted: var(--color-text-muted);
--radius-control: var(--radius-control);
--spacing-4: var(--space-4);
}
Now bg-surface, text-muted, rounded-control, and p-4 all exist as utility classes, generated from your tokens. No config file, no rebuild loop, no JavaScript indirection. When tokens.css changes, the utilities change.
The bits that bite you
A few war stories worth flagging.
Naming collisions with Tailwind defaults
If your semantic token is called gray-500 and Tailwind also ships a gray-500, the override behavior depends on whether you use @theme or @theme inline. We've shipped subtle bugs where the designer's gray scale silently lost to Tailwind's. Fix: prefix semantic tokens (ui-gray-500) or fully replace Tailwind's palette with @theme and accept that you own all the colors now.
Aliases that don't resolve
Figma lets you reference variables across collections. Tokens Studio sometimes exports these as {primitives.blue.500} strings instead of resolved values. If your Style Dictionary preprocessor isn't configured for this, you get literal string values in CSS — color: {primitives.blue.500}; — which fails silently in browsers. Always render a sample page in CI and screenshot-diff it.
Mode explosion
Figma supports up to four modes per collection on the Enterprise plan. Resist the temptation to use them for everything. We had a project with Light/Dark × Desktop/Mobile × Brand-A/Brand-B and the resulting CSS was hard to reason about. Use modes for visual themes only; use media queries for breakpoints.
Making it survive a real team
A pipeline is only as good as the CI around it. Three things we now treat as non-negotiable:
- PR previews for token changes. When
tokens.jsonchanges, the CI job builds a Storybook or component gallery and posts the URL. Designers can see the actual rendered impact before merge. - Visual regression on the gallery. Even a basic Playwright screenshot suite catches the "I tweaked one primitive and broke twelve components" class of bug.
- A token changelog. Generated automatically from
tokens.jsondiffs. Engineers need to know whencolor-text-mutedshifted from#64748bto#71717a, even if no class names changed.
If you're working with an external design partner or agency, write the token contract into the SOW. "Designers own primitives and semantics, engineering owns component tokens" sounds obvious but eliminates a surprising amount of friction.
What we'd do on a new project
If we were starting today: Tokens Studio for the export, Style Dictionary v4 for the transform, Tailwind v4 with @theme consuming CSS variables. Skip the JavaScript config entirely. Set up the three-layer model on day one, even if you only have eight tokens — adding the layers later is far more painful than starting with empty ones. And put a Playwright screenshot test on a token gallery page before you merge the second PR, not the fiftieth.
If you want to talk through a design system migration or a Figma-to-code workflow on your own product, we do this kind of work — see our services for what that looks like in practice.
Want a team like ours?
72Technologies builds production software for the kind of teams who actually read this blog.
Start a project