Figma to Tailwind: matching a design system in utility classes without the drift

Figma to Tailwind without drift: map Figma variables to your Tailwind v4 theme so generated code can use token classes, not arbitrary values. Six steps.

Modeinspect

Figma to Tailwind stays on your design system when every Figma variable name matches a class your Tailwind theme can produce. A 24px gap named spacing/6 in the design matches gap-6 in the code, and the brand blue matches bg-brand-500.

Without that map, a plugin or an agent writes arbitrary values such as p-[13px] and bg-[#3b82f6]. They match the frame on the day and ignore every later change to the theme.

This guide maps Figma variables and styles to the Tailwind v4 theme in six steps, with the v3 config as the older path. The names run from the code to the file, not the other way. It applies to every route from a design to a pull request that ends in Tailwind classes someone reviews.

Key takeaways

  • Tailwind v4 creates utility classes from theme variables in an @theme block. --color-brand-500 gives you bg-brand-500, so a Figma variable with the same name has a class to land on (Tailwind docs).
  • Spacing in v4 is a multiplier: p-4 means calc(var(--spacing) * 4), and --spacing defaults to 0.25rem. Name Figma spacing variables by the multiplier, such as spacing/6 for 24px (Tailwind docs).
  • Figma exports variable modes to JSON (Figma Learn), and Style Dictionary turns design tokens into plain CSS variables (Style Dictionary). Neither writes a Tailwind theme.
  • Lint rules catch drift before it merges. enforce-canonical-classes flags arbitrary values that have a theme equivalent, and no-unknown-classes flags classes your theme does not define (enforce-canonical-classes, no-unknown-classes).

Why Figma to Tailwind drifts

A Figma value with no matching theme variable becomes an arbitrary value in the code. Tailwind's docs call arbitrary values "basically like inline styles", for when you "need to break out of those constraints to get things pixel-perfect" (Tailwind docs).

Figma's MCP server outputs React and Tailwind by default (Figma developer docs). One Figma forum user found in 2025 that "get_code outputs Tailwind classes without any reference to Figma variables" (Figma Forum).

We hear the same thing on our calls with customers: AI tools hardcode values instead of using the team's tokens. The Figma file lags too, so anything exported from it fights the tokens in the repo.

One design lead told us how it happens. Their design system owner had built many custom tokens in Figma while engineering settled on Tailwind's. Drift then came from two layers: designs using tokens the code did not have, and developers skipping the shared components.

Diagram titled One name, three places: color/brand/500 in Figma maps to --color-brand-500 in the theme and the class bg-brand-500. The hex value #3b82f6 ends up as bg-[#3b82f6], which theme changes miss.

Before you start

You probably own the Figma variables and have never opened the repo's CSS. You have been asked to use AI on the handoff. You pointed a plugin or Figma's MCP server at a frame, and p-[13px] came back in the diff where the file said spacing/6. Renaming things in Figma did not change the next diff. The rest is on the code side, and fits in one conversation with an engineer.

  • Your Tailwind version. v4 keeps the theme in CSS, and v3 keeps it in tailwind.config.js. Step 1 shows how to tell.
  • A Figma plan with modes, for light and dark. Modes are available on Education, Professional, Organization and Enterprise plans (Figma Learn).
  • Access to the CSS file, or an engineer who has it. The theme lives in code, so the mapping lands as a pull request.

Step 1: Find where your Tailwind theme lives

In Tailwind v4, the theme lives in a CSS file, so search the repo's CSS for @theme. Theme variables are "special CSS variables defined using the @theme directive that influence which utility classes exist in your project" (Tailwind docs). A variable in @theme creates classes, and a variable in :root does not.

In v3, the theme lives in tailwind.config.js, and the naming below works the same way there. Tailwind says JavaScript config files still work "for backward compatibility, but they are no longer detected automatically in v4", so the CSS loads them with @config (Tailwind upgrade guide). A tailwind.config.js with no @config line and no @theme block anywhere usually means v3.

Step 2: Map each Figma collection to a theme namespace

Each kind of Figma value maps to a Tailwind namespace, the prefix that decides which classes a theme variable creates (Tailwind docs).

Figma sideTailwind v4 namespaceExample class
Color variables--color-*bg-brand-500, text-brand-600
Spacing number variables (gap, padding)--spacing, or --spacing-* for named stepsgap-6, p-4
Corner radius number variables--radius-*rounded-card
Text style size and line height--text-* with --text-*--line-heighttext-body
Font family--font-*font-sans
Font weight--font-weight-*font-medium
Letter spacing--tracking-*tracking-tight
Shadows--shadow-*shadow-md

Spacing is the exception. One base value, --spacing, is multiplied by the class: p-<number> is padding: calc(var(--spacing) * <number>) (Tailwind docs). Spacing utilities "accept any value out of the box" in v4 (Tailwind blog), so 24px is gap-6 and a 4px Figma grid needs no theme variable per step.

Text styles carry several values, and Tailwind pairs them under one name. --text-body sets the size, and --text-body--line-height sets its line height (Tailwind docs), so one class carries both.

Step 3: Name Figma variables the way the classes read

Alexander's pipeline on dev.to has code follow Figma: export the variables, generate the config in a GitHub Action, keep the file as the source of truth (dev.to). Do the reverse. The theme ships and the file does not, so the theme owns the names and Figma takes them.

Name the Figma variable after the class it should become. A variable named color/brand/500 lines up with the theme variable --color-brand-500 and the class bg-brand-500. The file, the theme and the diff then show the same name.

Name spacing by the multiplier, not the pixels. A variable called spacing/6 with a value of 24 reads like gap-6, while spacing/24 reads like gap-24, which is 96px at Tailwind's default. Radius follows the color pattern: radius/card lines up with --radius-card and rounded-card.

Comparison

Name spacing by the multiplier. A variable called spacing/24 reads as gap-24, which is 96px.

Named by the multiplier

spacing/6 = 24pxreads asgap-6 = 24px

calc(var(--spacing) * 6)

24px
Named by the pixels

spacing/24 = 24pxreads asgap-24 = 96px

calc(var(--spacing) * 24)

96px

Both Figma variables hold 24px. The gaps are drawn at their real size with Tailwind's default --spacing of 0.25rem. The class multiplies it, so the name decides the gap, not the value.

Source: Tailwind CSS docs, padding

Then set each variable's code syntax. Figma says "a variable's code syntax will appear in code snippets in Dev Mode", with "one name per platform, including Web, Android, and iOS" (Figma Learn). Type the theme variable's name in the Web field and check the preview reads --color-brand-500.

Finally, scope the variables. Scopes "limit which properties the variable can be applied to", such as corner radius, or gap and padding in auto layout (Figma Learn). A spacing number then cannot end up on a corner.

Step 4: Move the values into the theme

For a small system, type the values into @theme by hand. An engineer can review it in one sitting.

For a larger one, export from Figma. Right-click a mode and choose Export mode, or a collection and choose Export modes, to get a JSON file (Figma Learn).

Run Style Dictionary on that file from the terminal and you get a CSS file of plain variables in :root, which Tailwind cannot turn into a utility class. Style Dictionary reads Design Tokens Community Group (DTCG) tokens and has no Tailwind output (Style Dictionary). Then move those variables into @theme, or point @theme inline at them, as below. The design tokens guide covers that pipeline in full.

Syncing automatically through Figma's API needs "a Full seat in an Enterprise org" (Figma developer docs).

Figma modes become light and dark values. Tailwind's colors docs show the pattern: values in :root and [data-theme="dark"], then @theme inline for variables that reference other variables (Tailwind docs).

:root {
  --app-canvas: #ffffff;
  --app-ink: #111827;
}

[data-theme="dark"] {
  --app-canvas: #0b0f19;
  --app-ink: #f9fafb;
}

@theme inline {
  --color-canvas: var(--app-canvas);
  --color-ink: var(--app-ink);
}

Once your palette is complete, put --color-*: initial at the top of your @theme block, before your own colors, as Tailwind's example does. Default classes such as bg-red-500 are then gone, white and black included, so define those too (Tailwind docs).

Step 5: Make generated code use the theme

A theme only helps if the tools that write code use it. The classes come from three kinds of tool: agents, plugins and visual editors. With Figma's MCP server, ask for get_variable_defs on the selection. It "returns the variables and styles used in your Figma selection, such as colors, spacing, and typography" (Figma developer docs).

Without the theme, generated classes copy the frame's pixels. bg-[#3b82f6] and bg-brand-500 draw the same blue today, but only bg-brand-500 changes when the theme does.

Example

Both class lists draw the same card. Only the theme classes follow the next theme change.

Copied from the frame

Invite your team

Members can comment on every page.

Send invite
cardflexgap-[16px]p-[24px]rounded-[12px]avatarbg-[#3b82f6]
Named from the theme

Invite your team

Members can comment on every page.

Send invite
cardflexgap-4p-6rounded-cardavatarbg-brand-500

Both cards render from real CSS with the same values. Bracketed classes copy the frame's pixels. Theme classes name a variable, so a change to --color-brand-500 or --radius-card reaches only the second card.

Then add rules. Figma's example rules include these lines (Figma developer docs):

  • "Avoid hardcoded values, use design tokens from Figma where available"
  • "Replace Tailwind utility classes with the project's preferred utilities/design-system tokens when applicable"

The Figma MCP server guide covers setup, and the Figma and Cursor workflow shows a full design system rules file.

Plugins differ in what they know about your theme. Locofy lets you "import your custom Tailwind config files" by pasting a tailwind.config.js, and its docs do not mention v4 (Locofy docs). FigmaToCode can "Generate Tailwind 4 syntax" and "round spacing and colors to nearby Tailwind tokens", but its README says nothing about reading your theme (GitHub). Anima generates a tailwind.config for you, which is a second theme to reconcile with yours (Anima docs).

Matching Figma components to your React components is a separate job, covered in turning Figma frames into React components.

Step 6: Catch drift before it merges

A lint rule makes the theme a check, not a request. Once it runs, p-[13px] fails lint whether a plugin, an agent or a person typed it. eslint-plugin-better-tailwindcss has two rules for this.

enforce-canonical-classes flags a class when "arbitrary values or variants are used while a predefined value exists". It needs Tailwind 4.1.15 or later (enforce-canonical-classes docs).

no-unknown-classes flags "classes that are not defined in your Tailwind CSS config file" (no-unknown-classes docs). That catches a variable renamed in Figma but not in the theme. Alexander describes a bg-brand-primary-500 class that "just silently fails" when the config is out of date (dev.to).

For a stricter gate, eslint-plugin-tailwindcss has no-arbitrary-value, off by default and suggested only for teams that stick strictly to their config's presets (no-arbitrary-value docs). Turn it on once the theme covers what designers use.

Review

Three classes that miss the theme fail lint before the merge.

components/invite-card.tsx
+<div className="flex gap-[24px]">
+ <h3 className="text-body">Invite your team</h3>
+ <button className="bg-brand-primary-500">
+ Send invite
+ </button>
+ <p className="text-[#5b6472]">Expires in a week</p>
+</div>
  1. enforce-canonical-classesgap-[24px] has a theme class, gap-6.
  2. no-unknown-classesbg-brand-primary-500 is not in the theme. The variable was renamed in Figma only, so the button would ship with no background.
  3. no-arbitrary-valueFlags every bracketed value, even text-[#5b6472], which no theme class matches. Turn it on once the theme covers what designers use.

Rules 1 and 2 come from eslint-plugin-better-tailwindcss. Rule 3 comes from eslint-plugin-tailwindcss and is off by default.

Without lint, search the diff for -[. Most matches are arbitrary values to check against the theme.

A visual editor for React and Tailwind instead of an export

Tools that edit the code directly skip the export and change the classes already in your components, so the question becomes whether their edits use your theme.

  • Onlook reads a Next.js project's Tailwind config and theme tokens (Onlook), and focuses on "Next.js and TailwindCSS for now" (GitHub).
  • Subframe has first-class support for React and Tailwind (Subframe docs).
  • v0 Design Mode lets you adjust an element's styles in the live preview and detects Tailwind (v0 docs). It works on your own code once v0 imports your GitHub repo (v0 docs).
  • Builder Code says it uses your components and tokens (Builder Code).
  • Cursor Design Mode lets you click an element in the running app, and Cursor's agent edits the code (Cursor docs).
  • Modeinspect runs on Next.js React codebases that use Tailwind. Select an element in the live app and change its padding, and the design panel offers values from your Tailwind theme. Apply writes the class, and the change leaves as a pull request. Tokens are offered, not enforced, so a value that matches no token becomes a one-off value, and a pasted Figma frame does not bring its variables along as tokens.

Whichever you try, make one spacing change and read the diff. It should use a class from your theme, not a bracketed value. What to look for in a React visual editor compares these tools by what they see of your code.

Frequently asked questions

Can Figma export Tailwind classes directly?

Not from Dev Mode's code panel. Its built-in languages are CSS for web, SwiftUI or UIKit for iOS, and Compose or XML for Android (Figma Learn). Figma's MCP server does output React and Tailwind by default (Figma developer docs), though one Figma forum user found its classes had no reference to Figma variables (Figma Forum). Plugins and visual editors that edit your code are the other routes.

How do I convert Figma to Tailwind CSS?

Put the values in your Tailwind theme, name each Figma variable after the class it should become, and make the tools that write code read that theme. A color variable named color/brand/500 then lines up with --color-brand-500 and the class bg-brand-500. A lint rule catches whatever still slips through as a bracketed value.

How do I go from Figma to Tailwind with the MCP server?

Ask for get_variable_defs on the selection, which returns the variables and styles the selection uses (Figma developer docs). Then add rules that point the agent at your theme instead of hardcoded values. Check the diff against @theme, because the classes match your system only once the theme has the names. Seats, limits and setup for the Figma MCP server are covered separately.

Is there a Figma to Tailwind plugin?

Several. Locofy imports a pasted tailwind.config.js, and its docs do not mention v4. FigmaToCode writes Tailwind 4 syntax and can round values to nearby Tailwind tokens, but its README says nothing about reading your theme. Anima generates its own tailwind.config, which is a second theme to reconcile with yours. None of them makes the classes match your system until your theme has the names.

How do Figma variables and styles map to Tailwind?

By namespace, the prefix that decides which classes a theme variable creates (Tailwind docs). Color variables go to --color-*, corner radius to --radius-*, font families to --font-*, and text style sizes to --text-* with a paired line height. Spacing usually needs only one base value, --spacing, because the class multiplies it.

Does mapping Figma variables to Tailwind work with v3?

Yes. The naming works the same way, but the values go in the theme section of tailwind.config.js instead of @theme (Tailwind upgrade guide). The single spacing multiplier, @theme inline, resetting a namespace with initial and the canonical class lint rule are new in v4.

How do Figma light and dark modes work in Tailwind?

Each mode becomes a set of CSS variables: light values in :root, dark values under [data-theme="dark"], and an @theme inline block that points the color names at them (Tailwind docs). One class, such as bg-canvas, then switches with the theme. Modes need an Education, Professional, Organization or Enterprise plan in Figma.

Can I sync Figma variables to Tailwind automatically?

Not through Figma's own API unless you are on Enterprise. Syncing through it needs "a Full seat in an Enterprise org" (Figma developer docs). On other plans, export each mode as a JSON file and convert it with a tool such as Style Dictionary. That gives plain CSS variables, which still have to move into @theme before Tailwind turns them into classes.

When should you use an arbitrary value in Tailwind?

When the design leaves the system on purpose, for the odd value a constrained set of tokens cannot reach (Tailwind docs). If the same bracketed value shows up twice, it probably belongs in the theme.

How do I stop Tailwind arbitrary values like p-[13px] from merging?

Add a lint rule. In eslint-plugin-better-tailwindcss, enforce-canonical-classes flags an arbitrary value when the theme already has one, on Tailwind 4.1.15 or later. no-unknown-classes flags classes the theme does not define. Without lint, search the diff for -[.

Do visual editors use my Tailwind theme?

Some read it, and the difference shows in the classes they write: a class from your @theme or a bracketed value. Onlook reads a Next.js project's Tailwind config and theme tokens, and Builder Code says it uses your components and tokens. Modeinspect's design panel offers values from your theme but does not enforce them. What to look for in a React visual editor compares them.

Where to start this week

Search the repo's CSS for @theme. If it is there, you are on v4 and its names are the ones your Figma variables should carry. Then grep the last diff for -[: every match is a value with no theme name, and the first to rename on both sides. Pick your route from how a design becomes a pull request, or continue with the Figma to Next.js workflow.