Design Token Pipeline: Figma → Style Dictionary → CSS/Tailwind
Build a real design token pipeline from Figma Variables to Style Dictionary to Tailwind v4 CSS custom properties — no sync drift, no manual copy-paste.
Why Your Current Token Setup Is Probably Broken
Honestly, most design token workflows are just a Notion doc with hex codes that someone updates manually every six months. That's not a pipeline. That's a prayer.
The problem shows up in production. A designer updates --color-brand-primary from #6366F1 to #4F46E5 in Figma, sends a Slack message, and then waits. Three weeks later, half the components still use the old value because someone hardcoded it in a one-off utility class.
A real pipeline means one source of truth — Figma Variables — that flows into Style Dictionary, which outputs CSS custom properties and a Tailwind v4 config extension. Touch one JSON file and every output updates. That's what this article walks through.
We're using Style Dictionary v4.0.1 here. Earlier versions have a different config format, so double-check which version you're on before copying anything.
Setting Up Figma Variables for Export
Figma's Variables panel (available since late 2023) is the starting point. You'll want to organize your variables into collections: Primitives, Semantic, and optionally Component. Don't skip this structure — Style Dictionary maps directly to it.
Primitives are raw values: color/blue/500 = #4F46E5, spacing/4 = 16px, radius/md = 8px. Semantic tokens reference primitives: color/brand/primary = {color.blue.500}. This two-layer approach is what lets you swap an entire theme without touching component code.
To export, use the Tokens Studio for Figma plugin or Figma's own REST API with the /v1/files/:key/variables endpoint. Tokens Studio writes a tokens.json that Style Dictionary can consume directly. If you're going the API route, you'll need to transform the response shape yourself — it's doable but adds a build step.
Style Dictionary Config That Actually Works
Style Dictionary reads your token JSON and transforms it into whatever output format you need. Here's a config that outputs both CSS custom properties and a Tailwind v4 @theme block:
// style-dictionary.config.js
import StyleDictionary from 'style-dictionary';
export default {
source: ['tokens/**/*.json'],
platforms: {
css: {
transformGroup: 'css',
prefix: 'empire',
buildPath: 'src/styles/generated/',
files: [
{
destination: 'tokens.css',
format: 'css/variables',
options: {
outputReferences: true,
},
},
],
},
tailwind: {
transformGroup: 'js',
buildPath: 'src/styles/generated/',
files: [
{
destination: 'tailwind-tokens.js',
format: 'javascript/esm',
},
],
},
},
};The outputReferences: true option is important — it keeps the CSS output using var(--empire-color-brand-primary) references rather than flattening everything to raw hex values. That's what makes theme switching work at runtime without a JS dependency.
Run npx style-dictionary build --config style-dictionary.config.js and you'll get two files. Wire the CSS file into your global stylesheet with @import './generated/tokens.css' and you're done with the CSS side.
Wiring Tokens into Tailwind v4
Tailwind v4.0.2 changed everything about how you extend the config. There's no more tailwind.config.js by default — configuration moves into your CSS file via @theme. This actually plays really well with generated token files.
/* src/styles/global.css */
@import 'tailwindcss';
@import './generated/tokens.css';
@theme {
--color-brand-primary: var(--empire-color-brand-primary);
--color-brand-secondary: var(--empire-color-brand-secondary);
--color-surface-glass: rgba(255, 255, 255, 0.15);
--spacing-component-gap: 8px;
--radius-card: var(--empire-radius-md);
--radius-button: var(--empire-radius-sm);
}Now bg-brand-primary and gap-component-gap work as Tailwind utilities automatically. The chain is: Figma Variable → tokens.json → Style Dictionary → tokens.css → @theme → utility class. Changing the Figma value triggers a rebuild and every utility updates.
Worth noting: if you're still on Tailwind v3, you'd use theme.extend in tailwind.config.js instead. The generated tailwind-tokens.js file from Style Dictionary slots in there cleanly. Either way, the token pipeline itself doesn't change — just the consumption end.
Handling Multi-Theme Token Sets
What happens when you need a dark theme or a customer-branded white-label? This is where the semantic layer pays off. You define semantic tokens once, then override them per theme.
Create separate token files: tokens/themes/dark.json and tokens/themes/light.json. Each one overrides semantic values only — never primitives. Then in your CSS, scope them with [data-theme='dark']:
/* generated/tokens.css — simplified output */
:root {
--empire-color-brand-primary: #4F46E5;
--empire-color-surface-bg: #FFFFFF;
--empire-color-text-default: #111827;
}
[data-theme='dark'] {
--empire-color-brand-primary: #818CF8;
--empire-color-surface-bg: #0F172A;
--empire-color-text-default: #F9FAFB;
}Your components don't change at all. They reference var(--empire-color-surface-bg) and the browser handles the rest. If you're building a theme toggle in React, you just flip the data-theme attribute on <html> — no CSS-in-JS, no context rerenders, nothing extra.
For white-labeling, add a third file: tokens/themes/customer-acme.json. Style Dictionary builds it as a separate CSS file you load conditionally. One pipeline, N themes.
Automating the Figma Sync in CI
Manually exporting tokens defeats the purpose. You want the pipeline to run on every Figma publish — or at minimum on every PR that touches the token files.
Tokens Studio has a GitHub sync feature that commits tokens.json directly to your repo when a designer hits Publish. Pair that with a GitHub Action that runs style-dictionary build and commits the generated files back to the branch. The whole cycle takes about 12 seconds.
Does every team need this level of automation? Probably not early on. If you have one designer and two engineers, a manual npm run tokens:build script you run before PRs is totally fine. But once you hit four or more designers, automated sync prevents the drift that kills design systems.
One thing to watch: generated files in version control get noisy in diffs. Consider adding src/styles/generated/ to .gitattributes as a linguist-generated path so GitHub collapses those diffs by default. Small quality-of-life thing, but your teammates will thank you. For deeper spacing consistency, the approach pairs well with a solid CSS spacing system.
Token Naming Conventions That Scale
The hardest part of a token pipeline isn't the tooling. It's naming. Bad names break the abstraction and you end up with tokens like --color-blue-that-we-use-for-buttons committed to production.
The naming pattern that holds up is {category}/{concept}/{variant}/{state}. So: color/brand/primary/hover, spacing/component/gap/sm, radius/interactive/default. You don't always need all four segments — color/neutral/900 is fine for primitives.
Avoid naming tokens after their visual output. --color-red-500 is a primitive. --color-feedback-error is a semantic token. If a designer changes the error color from red to orange, the semantic token stays valid. If you hardcoded --color-red-500 in your button component, you now have orange text with a broken name.
This also connects to building an accessible color system — semantic naming makes WCAG compliance auditable because you can check --color-text-on-brand-primary contrast ratio once and trust it everywhere.
Integrating Tokens with Empire UI Components
Empire UI's 40 visual styles all resolve to CSS custom properties at the component layer. When you bring in the token pipeline, your generated tokens.css just feeds those properties. No changes to the component code.
The component side looks like this. A card component references var(--empire-color-surface-glass) — which in the glassmorphism style is rgba(255, 255, 255, 0.15) with a backdrop-filter: blur(12px). If you override that token in your theme file, every card updates. The glassmorphism style guide has more on how that specific token stack works.
For icon components, tokens control stroke width and fill color but not the SVG paths themselves. Check the icon system docs if you're building icon tokens — there are some non-obvious size mapping decisions that'll save you time.
The end state you're aiming for: a designer changes a value in Figma, clicks Publish, CI builds tokens, the component library ships an update, and product engineers get updated utilities via a package bump. No Slack messages needed. That's the pipeline.
FAQ
Not out of the box. Style Dictionary v4.0.1 can output a CSS file with custom properties, but you write your own @theme block that references those properties. There's no built-in Tailwind v4 format yet — though the community has published a few custom formatters on npm you can drop in if you don't want to write it yourself.
Yes. The Figma REST API at /v1/files/:key/variables returns your variable collections as JSON. You'll need to write a small transform script to reshape that response into the flat token JSON that Style Dictionary expects. Tokens Studio handles this transform for you automatically, which is why most teams reach for it first.
It depends on your team. Committing them means every developer can run the app without running the build step first, which reduces onboarding friction. The downside is noisy diffs. A middle path: commit them but mark them as generated in .gitattributes so GitHub collapses them in PR views.
Create a separate token file per component — like tokens/components/button.json — and add it to Style Dictionary's source glob. Style Dictionary merges all source files before building. Component tokens should reference semantic tokens, not primitives, so they still respond to theme changes.
Style Dictionary's javascript/esm output gives you a plain object you can use to generate a union type. There are community transforms that emit a TypeScript declaration file alongside the JS output. For CSS custom properties, consider using @csstools/css-color-parser or a similar utility to validate values at build time rather than runtime.
Style Dictionary has a built-in transform called size/px that converts numeric values to px strings. For rem conversion, you write a custom transform that divides by 16 (or whatever your base font size is). Add it to your config's transforms array and it runs automatically on all size-category tokens during the build.