NewMCP Apps — interactive section cards now render inside your AI chatRead the changelog
Engineering

From Figma variables to CSS: a token pipeline that does not drift

Variables in Figma, custom properties in CSS, and one JSON file between them. How we keep 2,400 sections on the same palette without anyone copying a hex code.

DR2 min read
On this page 4 sections

In short

Export Figma variables to one JSON file, generate CSS custom properties from it at build time, and make components read only the semantic layer. When the JSON is the only place a value lives, design and code cannot drift apart.

Key takeaways

  • Keep one token file; generate everything else from it.
  • Components read semantic tokens, never primitives.
  • Generate CSS at build time so a missing token fails the build.
  • Version the token package and changelog every change.

Token pipelines usually fail at the hand-off. The designer updates a variable in Figma, someone forgets to update the code, and three months later the product has two shades of the same blue. The fix is boring and reliable: one file, generated everywhere.

Start with one source file#

Figma’s variables export as JSON. We keep that file in the repository as tokens.json and treat it as the only place a colour, a space or a radius is defined. Everything else — CSS, the documentation site, the MCP server’s DESIGN.md — is generated from it.

json
{
  "color": {
    "violet-500": { "value": "#7C5CEF" },
    "action-primary": { "value": "{color.violet-500}" }
  },
  "radius": { "card": { "value": "12px" } }
}

Generate the CSS at build time#

A small script walks the JSON and writes custom properties. Because it runs in the build, a token a component asks for but the file does not have fails the build instead of rendering as nothing.

css
:root {
  --color-violet-500: #7C5CEF;
  --color-action-primary: var(--color-violet-500);
  --radius-card: 12px;
}

Two layers, one rule#

  • Primitive tokens hold raw values: violet-500 is #7C5CEF and means nothing else.
  • Semantic tokens hold intent: action-primary points at a primitive.
  • Components read semantic tokens only — that one rule makes a rebrand a one-line change.

Keep it honest in CI#

A lint rule that fails on raw hex values outside the token file takes an afternoon to write and removes a whole category of drift. We run it on every pull request; it is the cheapest check we have.

CheckWhat it catchesCost
Raw hex lintColours typed by handAn afternoon
Missing token fails buildA component asking for a token that does not existFree with generation
Snapshot at 3 breakpointsA token change that breaks a layoutMinutes per run

None of this is clever. That is the point: a pipeline nobody has to remember to run is the only one that survives a busy quarter.

How we know this

We migrated the library’s 2,400 sections to this pipeline between May and July 2026.

Related questions

Can Figma variables be the source of truth?

Yes, if they are exported to a file in the repository and that file is the only place code reads values from.

Should components use primitive tokens?

No. Components use semantic tokens, which point at primitives. That indirection is what makes a rebrand cheap.

How do you stop hard-coded colours?

A lint rule in CI that fails on raw hex values outside the token file.

Comments

No comments yet — be the first to share a thought.

Leave a comment

Your email is never shown. Comments are reviewed before they appear.

Links in comments are marked ugc nofollow.

Keep reading

All articles