Guides · April 12, 2023
Design Tokens from Figma Variables to Tailwind v4
Figma Variables and Tailwind v4's CSS-first @theme are built on the same idea: name a value once, reference it everywhere. Here is the exact workflow for turning one into the other without losing sync between design and code.
By Polo Themes
The fastest reliable path from Figma Variables to Tailwind v4 is a two-tier token model: export your Figma primitive and semantic Variable collections as JSON, run them through a small transform script into CSS custom properties, and register those properties inside a single @theme block in your global stylesheet. Tailwind v4 reads CSS variables natively, so once a variable exists in @theme it becomes a real utility class (bg-surface, text-muted-foreground, and so on) with no config file, no plugin, and no separate source of truth to keep in sync by hand.
This matters more than it sounds like it should. Most teams that adopt design tokens end up maintaining them in three places at once — a Figma library, a JSON export, and a hand-written Tailwind config — and the three drift within a few sprints. Tailwind v4's CSS-first configuration removes one of those layers entirely, and Figma's Variables feature (not styles, not plugins — the native Variables panel introduced in 2023 and matured through 2024–2025) gives you a first-class primitive/semantic structure that maps almost one-to-one onto how a well-built Tailwind theme should be organized. This guide walks through the full pipeline, the mistakes that break it, and the alias structure that keeps light/dark theming and multi-brand support cheap instead of painful.
Why Variables and @theme Are the Same Idea Twice
Figma Variables let you define a value — a color, a number, a string, a boolean — once, give it a name, and reference that name from any property on any layer. Change the variable and every layer referencing it updates. Tailwind v4's @theme directive does the same thing for CSS: define a custom property once inside @theme, and Tailwind generates a matching utility class automatically, while every element using that utility inherits updates to the underlying variable. The two systems were designed around the same mental model — indirection through named values — which is exactly why a direct mapping between them works so well and why bolting Figma exports onto Tailwind v3's JavaScript tailwind.config.js always felt like translating between two different paradigms instead of one.
The practical implication is that your Figma Variable collections and your Tailwind @theme block should have the same shape. If they diverge — if Figma has a color/brand/primary/600 variable with no Tailwind equivalent, or Tailwind has a spacing scale nobody bothered to add to Figma — you have already lost the sync you were trying to build. Treat structural parity between the two as the actual deliverable, not the code that generates it.
Step 1: Structure Figma Variables as Two Tiers, Not One
The single most common mistake in token systems — in Figma, in Tailwind, in Style Dictionary, anywhere — is a flat token list with no separation between raw values and their meaning. A two-tier structure fixes this and should be set up in Figma before you export anything.
- Primitives collection: raw, meaningless-on-their-own values — blue-500, space-4, radius-md, duration-200. These rarely change once set and describe *what the value is*, not what it's for.
- Semantic collection: named by role, referencing primitives — surface, surface-muted, foreground, border, accent, accent-foreground, destructive. These describe *what the value is for*, and this is the layer your components should actually consume.
In Figma, this means your semantic Variables collection has entries whose values are references to primitives (Figma's Variables panel supports this directly — a Variable's value can itself be another Variable), not hard-coded hex codes. Do the linking inside Figma, not after export; if your semantic tier resolves to literal values at export time you have baked in a dependency you will have to redo by hand every time a primitive shifts.
This is also the exact structure Tailwind v4 expects. A well-built @theme block separates primitive scales (a color ramp, a spacing scale) from semantic role tokens that components actually reference, and it is the structure used across modern component systems — including shadcn/ui's CSS variable convention, which most teams building on Tailwind v4 are already following whether they call it that or not. If you are designing an e-commerce interface from scratch, this is also the structure our Figma UI kits are built around, precisely so the handoff below is mechanical rather than a redesign.
Step 2: Export Variables to JSON
Figma does not (as of this writing) offer a built-in "export to Tailwind" button, and you should be skeptical of any plugin that claims to do the whole pipeline for you — the mapping decisions in the next step are design decisions, not mechanical ones, and a black-box plugin will make them for you badly. What you want from Figma is a clean JSON export of your Variable collections, modes, and values. The Figma REST API's GET /v1/files/:key/variables/local endpoint returns exactly this: collections, modes (Light/Dark, Brand A/Brand B, etc.), and every variable's resolved value per mode, including alias references between variables.
If you would rather stay inside the Figma UI, the Variables Import/Export community plugins that follow the W3C Design Tokens Community Group format are a reasonable second choice — that format ($value, $type, and $description keys, with {alias.path} references) has become the de facto interchange standard, and tools across the ecosystem (Style Dictionary, Tokens Studio, Supernova) increasingly read and write it. Whichever route you take, the output you want is a JSON tree with modes as top-level or nested keys and alias references preserved as references, not pre-resolved values — resolving aliases is a transform-step concern, not an export-step one.
Step 3: Transform JSON into CSS Custom Properties
This is the step teams most often try to skip by hand-copying values into CSS, and it is the step that causes the most drift six months later. Write a small script — fifteen to forty lines is typical — that walks the exported JSON and emits CSS custom properties, one file per mode if you support light/dark or multi-brand theming.
The transform has three responsibilities, and each one maps to a real bug if skipped. First, flatten naming: Figma's nested collection/group/name structure (color/surface/muted) needs to become a valid, kebab-cased CSS custom property name (--color-surface-muted). Second, resolve or preserve aliases deliberately: a semantic variable that references a primitive should become a var() reference to the primitive's custom property (--color-surface: var(--color-blue-50);), not the primitive's resolved hex value — this is what keeps a later primitive change (rebranding your blue) propagating through semantics automatically, exactly like it does inside Figma. Third, split by mode into separate scopes: Light-mode values go on :root, dark-mode values go under a @media (prefers-color-scheme: dark) block or a [data-theme="dark"] attribute selector (or both, with the attribute selector given higher specificity so a manual toggle overrides the OS setting) — never emit both modes' values onto the same selector, which is the single most common cause of "my dark mode half-works" bug reports.
Keep this script under version control alongside the design tokens it consumes, and run it in CI whenever the exported JSON changes, rather than running it manually and committing the output by hand. A token pipeline that requires a human to remember to re-run a script is a token pipeline that will silently go stale.
Step 4: Register the Variables in Tailwind v4's @theme
Tailwind v4 dropped tailwind.config.js as the primary configuration surface in favor of CSS-native configuration through the @theme directive inside your main stylesheet. This is the step where your generated custom properties become real Tailwind utilities — and it is also where teams most often go wrong by defining variables as @theme entries at both the primitive and semantic level, which floods the generated utility set with classes nobody should use directly (you do not want bg-blue-500 available right next to bg-surface if the whole point of the semantic tier is that components never reach for the raw color).
The cleaner pattern: define primitives as plain CSS custom properties on :root (not inside @theme), and only put the semantic layer inside @theme, mapped from Tailwind's own namespace conventions — --color-surface, --color-foreground, --radius-md, --spacing-* — using var() references back to the primitives. This gives you utilities like bg-surface, text-foreground, and rounded-md generated automatically, while the primitive scale stays available for the rare cases (usually inside the token pipeline itself, or a design-system storybook) that need it, without polluting IntelliSense and code review with raw color utilities that shouldn't be used in components.
The payoff of doing this correctly is that theme switching becomes a one-line CSS concern instead of a JavaScript one: toggling data-theme="dark" on the root element flips every semantic custom property to its dark-mode value, and every component using bg-surface or text-muted-foreground repaints correctly with zero component code touched. Multi-brand theming works the same way with a different attribute or a build-time variable swap — the component layer never needs to know a second brand exists.
Step 5: Keep the Loop Closed
A one-time export is a snapshot, not a system. The teams that keep Figma and Tailwind honestly in sync treat the transform script from Step 3 as a build step, not a manual chore, and they gate merges on it: a pull request that changes the CSS custom properties without a corresponding Figma Variable change (or vice versa) is a signal something drifted, not a normal diff. Some teams wire the Figma REST API export directly into CI on a schedule or webhook and fail the build if the generated CSS diff is unexpectedly large; that is worth the setup cost once a design system has more than a couple of contributors, but is overkill for a one-person project where a documented manual step is fine.
It is also worth deciding, up front, who owns naming. The semantic tier is a vocabulary — surface, muted, accent, destructive — and vocabulary drift (one designer adds danger while the codebase already has destructive) is a more common failure mode than any technical step in this pipeline. Put the semantic token list in a shared document or a comment block at the top of your CSS file, and treat adding a new semantic name with the same scrutiny you'd give adding a new prop to a shared component.
Where AI Design-to-Code Tools Fit — and Where They Don't
AI design-to-code tools have gotten genuinely good at pattern-matching a Figma frame into markup and utility classes, and it is tempting to let one generate the entire token layer along with the components. Resist that for the token layer specifically. Component generation is disposable — a bad AI-generated card component gets deleted and regenerated with no lasting cost. A bad AI-generated token structure is the opposite: every component built on top of it inherits the mistake, and unwinding a flat or inconsistently named token set after fifty components already reference it is a multi-day refactor, not a five-minute fix. Let AI tools consume a token layer you designed deliberately using the two-tier structure above; don't let them invent the token layer as a side effect of generating a page.
This is also where Model Context Protocol (MCP) servers for design tools are heading in a genuinely useful direction: rather than an AI agent screen-scraping a rendered Figma frame and guessing at colors, an MCP-based integration can query the Variables API directly and get exact names, exact values, and exact alias relationships — the same JSON this guide's Step 2 describes, available to an agent on demand instead of only to a build script on a schedule. The mechanics are identical either way; what changes is who's reading the export and when.
Frequently Asked Questions
Do I need Figma's paid Variables feature, or do Styles work too?
Variables (not Styles) are what support aliasing, modes, and the REST API export this pipeline depends on. Color and text Styles can be exported too, but they don't carry mode or alias information the same way, so you lose the light/dark and primitive/semantic structure that makes the sync worthwhile. If your team is still on Styles, migrating your color and spacing values to Variables is worth doing before building this pipeline, not after.
Can I skip the transform script and just hand-copy values into @theme?
For a handful of colors on a single small project, sure. Past roughly a dozen tokens or the first time you add a second mode (dark mode, a second brand), hand-copying becomes the exact drift source this guide is trying to prevent. A short script is cheap insurance.
Does this approach work with shadcn/ui components?
Yes — shadcn/ui's convention is CSS custom properties consumed through Tailwind, using largely the same semantic names (background, foreground, border, accent, destructive) this guide recommends. If you're already using shadcn/ui, aligning your Figma semantic tier to its variable names means components can be dropped in without renaming a single class.
What about Tailwind v3 — does any of this apply?
The Figma-side steps (Variables structure, JSON export, the transform script) are identical. The difference is only in Step 4: Tailwind v3 registers tokens through tailwind.config.js's theme.extend, referencing the same generated CSS custom properties via var(), rather than through a CSS-native @theme block. The pipeline and the discipline are the same either way — v4 just removes a layer of JavaScript indirection.
For a broader look at building commerce interfaces on top of a token system like this, browse our Guides section or the Figma UI kits built around the same primitive/semantic structure described here.