Design Token Automation: CI Pipeline from Figma to Production
Automate design tokens from Figma to production CSS in one CI pipeline. No manual copy-paste, no drift. Here's exactly how to wire it up with Style Dictionary.
Why Manual Token Sync Is Killing Your Design System
Honestly, if your team is still copy-pasting hex values from Figma into a CSS file by hand, you're burning engineering hours on work that a CI pipeline can do in 30 seconds.
The drift problem is real. A designer updates --color-brand-primary from #5B4FCF to #6355D8 in Figma. Two weeks later, production still shows the old purple. No one noticed. No one is "at fault". The system just doesn't have a forcing function.
Design token automation closes that gap. The idea is simple: Figma is the source of truth, a token file is exported (or generated via API), Style Dictionary transforms it into CSS variables, SCSS maps, JS constants, whatever you need — and CI deploys those outputs automatically on every merge.
This isn't theoretical. Teams running this pipeline catch token drift before it ships. And once it's wired up, designers and developers stop arguing about which shade of grey is correct.
The Token Pipeline Architecture: Figma → JSON → Outputs
The pipeline has four stages. Export, transform, lint, publish. Each stage is a discrete step you can inspect, debug, and replace independently.
Stage one is export. The Figma Tokens plugin (or the newer Figma Variables REST API introduced in Figma v116) exports a tokens.json file that follows the W3C Design Token Community Group spec. Your CI job pulls this file — either via a webhook trigger from Figma or on a scheduled cron.
Stage two is transformation with Style Dictionary v4.x. You write a config.js that maps token categories to output formats. One input file, multiple outputs: tokens.css, tokens.js, _tokens.scss. Style Dictionary handles the flattening, aliasing, and type coercion.
Stage three is linting. You run a token linter (Token Lint, or a custom script) that checks for missing required tokens, contrast ratio failures, and naming convention violations. Fail fast here, not in production.
Stage four is publish. The built output files get committed back to the repo on a dedicated chore/token-sync branch, a PR is opened automatically, and your normal review process takes over. Or, if you prefer fully automated, you merge straight to main and let your Tailwind v4.0.2 JIT compiler pick up the changes on the next build.
Setting Up Style Dictionary v4 for Multi-Platform Output
Style Dictionary is the industry standard for token transformation. It's been around since 2017 and the v4 release (dropping CJS, going pure ESM) cleaned up a lot of the configuration awkwardness.
Here's a real sd.config.js that outputs CSS variables, a TypeScript constants file, and a Tailwind-compatible JS object in one run:
// sd.config.js — Style Dictionary v4.x
import StyleDictionary from 'style-dictionary';
const sd = new StyleDictionary({
source: ['tokens/tokens.json'],
platforms: {
css: {
transformGroup: 'css',
prefix: 'emp',
buildPath: 'src/tokens/built/',
files: [
{
destination: 'tokens.css',
format: 'css/variables',
options: { outputReferences: true },
},
],
},
ts: {
transformGroup: 'js',
buildPath: 'src/tokens/built/',
files: [
{
destination: 'tokens.ts',
format: 'javascript/es6',
},
],
},
tailwind: {
transformGroup: 'js',
buildPath: 'src/tokens/built/',
files: [
{
destination: 'tailwind-tokens.cjs',
format: 'javascript/module',
},
],
},
},
});
await sd.buildAllPlatforms();The prefix: 'emp' maps every token to --emp-color-brand-primary, which keeps Empire UI tokens namespaced and prevents collisions with third-party libraries. You'll want something equivalent in your own system. Collisions are subtle bugs — the kind that only show up on specific pages.
Wiring the CI Job in GitHub Actions
The actual CI configuration is simpler than most people expect. Here's a GitHub Actions workflow that runs on push to main and on a nightly schedule. The nightly schedule catches cases where a designer updates Figma without triggering a code event.
# .github/workflows/token-sync.yml
name: Design Token Sync
on:
push:
branches: [main]
paths:
- 'tokens/**'
schedule:
- cron: '0 6 * * *' # 06:00 UTC daily
jobs:
sync-tokens:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
- name: Setup Node 20
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- name: Pull tokens from Figma
env:
FIGMA_TOKEN: ${{ secrets.FIGMA_PERSONAL_TOKEN }}
FIGMA_FILE_ID: ${{ secrets.FIGMA_FILE_ID }}
run: node scripts/pull-figma-tokens.mjs
- name: Build tokens
run: node sd.config.js
- name: Lint tokens
run: pnpm token-lint
- name: Open PR if tokens changed
uses: peter-evans/create-pull-request@v6
with:
commit-message: 'chore(tokens): sync from Figma'
branch: chore/token-sync
title: 'chore(tokens): automated design token sync'
body: 'Auto-generated by token-sync workflow. Review before merging.'
labels: 'design-tokens,automated'The pull-figma-tokens.mjs script calls the Figma Variables REST API with your personal access token and writes the result to tokens/tokens.json. Keep that script simple and auditable — it's the one thing that talks to an external service.
One thing worth noting: don't use secrets.GITHUB_TOKEN if you need the PR to trigger downstream CI. GitHub's built-in token can't trigger other workflows to prevent infinite loops. Use a PAT or a GitHub App token instead.
Integrating Token Outputs with Tailwind v4 and CSS Custom Properties
With Tailwind v4.0.2's new CSS-first configuration, wiring in your generated tokens is genuinely clean. You import the built CSS file and reference those custom properties in @theme.
If you've read our article on building a spacing system in CSS, you'll recognize the pattern: single source of truth, consumed at the framework level, no duplication.
/* app/globals.css */
@import '../tokens/built/tokens.css';
@theme {
--color-brand: var(--emp-color-brand-primary);
--color-brand-subtle: var(--emp-color-brand-subtle);
--color-surface: var(--emp-color-surface-default);
--color-overlay: rgba(255,255,255,0.15);
--spacing-xs: var(--emp-spacing-xs); /* 4px */
--spacing-sm: var(--emp-spacing-sm); /* 8px */
--spacing-md: var(--emp-spacing-md); /* 16px */
--spacing-lg: var(--emp-spacing-lg); /* 24px */
--spacing-gap: var(--emp-spacing-gap); /* 8px gap */
--font-sans: var(--emp-font-family-sans);
--radius-md: var(--emp-border-radius-md);
}Your Tailwind classes like bg-brand and text-brand-subtle now resolve through the token chain all the way back to Figma. Update the color in Figma, the pipeline runs, the PR lands, you merge — and bg-brand everywhere automatically reflects the new value. No find-and-replace.
Token Naming Conventions That Don't Break in Six Months
Naming is where most token systems fall apart. Not on day one — on day 180, when someone adds --color-blue-500 because they were in a hurry and didn't check if --color-brand-primary was the right answer.
Use three-tier semantic naming: category, role, variant. color.feedback.error.default, color.feedback.error.hover, spacing.component.card.padding. The category tells you what kind of token it is. The role tells you its semantic purpose. The variant handles states.
What you want to avoid is one-to-one mappings from Figma's auto-generated layer names. Figma will export Frame 423 / Rectangle 12 / Fill if you're not disciplined about naming in the Figma file itself. Work with your designer to agree on naming before the pipeline exists — fixing names after hundreds of components reference them is painful.
Also, if you're building a component library like Empire UI where color system design needs to work across 40 visual styles, you'll want alias tokens on top of base tokens. Base tokens are the raw values (color.base.purple.600 = #5B4FCF). Alias tokens are the semantic layer (color.brand.primary = {color.base.purple.600}). Components reference alias tokens only.
Handling Theme Variants and Dark Mode in the Token Pipeline
Dark mode isn't an afterthought in this pipeline — it's a first-class concern from the Figma export step. Figma Variables supports multiple modes per collection (Light, Dark, High Contrast). The export script pulls all modes and Style Dictionary generates a separate CSS block for each.
The output looks like this: a :root block with light values, a .dark block (or @media (prefers-color-scheme: dark)) with overrides. Components don't need to know about theming at all — they just use var(--emp-color-surface-default) and it resolves to the right value for the active mode.
If you've implemented a theme toggle in React, you're already consuming the CSS custom property layer. The pipeline just makes sure those properties are always accurate and never out of sync with Figma.
One edge case: handle token additions and removals separately. When a token is deleted in Figma, you don't want CI to silently remove a CSS variable that 80 components depend on. Add a deprecation step: mark the token as deprecated for one release cycle, emit a console warning in dev mode, then remove it in the next. This saves you from runtime errors in production.
Testing and Validating Token Changes Before They Ship
Can you trust that a token change in Figma won't break your UI? Only if you test it. Visual regression testing on token PRs is the last line of defence before a color change makes your buttons unreadable.
Run a Storybook build on every token PR. If you've set up your Storybook component library with Chromatic or Percy, the visual diff will immediately surface any components affected by the changed tokens. A 2px radius change on --emp-border-radius-md shows up as a diff on every card, button, and input that uses it.
Beyond visual regression, run a contrast ratio check as part of pnpm token-lint. Any foreground/background token pair that falls below WCAG AA (4.5:1 for normal text) should fail the build. This connects directly to the guidance in our WCAG accessibility guide — accessibility regressions from a color token change are one of the most common (and embarrassing) production incidents.
Token testing isn't glamorous work. But a failing CI check on a token PR is infinitely cheaper than an accessibility audit finding or a designer noticing their brand color shipped wrong to 50,000 users.
FAQ
CSS variables are the output format. Design tokens are the conceptual layer — the name, value, type, and description of a decision (like 'the primary brand color is purple'). Style Dictionary transforms tokens into CSS variables, JS constants, SCSS maps, iOS Swift files, and Android XML from the same source.
Yes. The Figma Variables REST API (available since Figma v116) requires a Figma Professional or Organization plan. If you're on the free tier, the Figma Tokens plugin (now Tokens Studio) can export to JSON on any plan and covers most use cases.
Use a fixed branch name like chore/token-sync and configure create-pull-request with force-push: true. This overwrites the branch on each run rather than creating a new commit on top of a stale one. The PR updates in place, and you avoid the conflict pile-up.
Yes. Define multiple source arrays and use Style Dictionary's include and source separation: shared base tokens go in include, brand-specific overrides go in source. Run buildAllPlatforms() once per brand with a different config. The output path uses the brand name as a subfolder, e.g., built/brand-a/tokens.css.
In your Style Dictionary config, set outputReferences: true in the CSS platform. This emits --emp-color-brand-primary: var(--emp-color-base-purple-600) instead of the resolved value. For JS outputs where CSS variables don't work, set outputReferences: false so the JS file gets the raw hex value directly.
Run node sd.config.js directly after manually editing tokens/tokens.json. Check the output files in src/tokens/built/. Then run your dev server and verify the CSS variables are applied. Once local runs are clean, push the config and let GitHub Actions handle the Figma API step.