Design systems live or die by one thing: consistency between what's designed and what's shipped. After years of watching Figma mockups drift from codebases, we became obsessed with closing that gap permanently.
On the Halcyon project, we built a 220-component design system from scratch in 14 weeks — and by launch day, the Figma file and the live product were pixel-for-pixel identical. Here's exactly how we did it.
TL;DR: Figma Variables + Tailwind v4 @theme + a 50-line Node.js sync script = zero design-dev drift. Jump to Step 3 if you just want the workflow.
The Problem with Traditional Handoffs
The classic workflow: designer makes a decision in Figma → exports specs → developer interprets and implements → drift begins immediately. Every subsequent design iteration requires another manual sync, and the gap widens with every sprint.
The only way to eliminate drift permanently is to make the design file and the codebase share the same source of truth — not just reference each other.
Step 1: Primitive vs Semantic Tokens
Figma's Variables feature is the missing link. The key is a two-layer token architecture that separates raw values from their intended usage:
// Primitive tokens (raw values)
color/navy/base → #0a192f
color/primary/base → oklch(79.5% 0.184 86.047)
// Semantic tokens (reference primitives — this is the key)
color/bg/page → {color/navy/base}
color/bg/surface → {color/navy/light}
color/accent → {color/primary/base}
Why the semantic layer matters
Using only primitive tokens means every component directly references a raw color. Want to swap your brand color? You're updating hundreds of components individually. Semantic tokens add one crucial indirection — components reference meaning, not value. A full rebrand becomes a one-line change.
Step 2: Exporting to Tailwind v4 @theme
With Tailwind CSS v4, design tokens live in CSS — not a JavaScript config file. This is a game-changer for synchronisation. Your @theme block is just text, which means it's trivially auto-generatable:
/* app.css — Auto-generated from Figma Variables. Do not edit manually. */
@import "tailwindcss";
@theme {
--color-primary: oklch(79.5% 0.184 86.047);
--color-primary-dim: rgba(239,177,0,0.07);
--color-primary-border: rgba(239,177,0,0.18);
--color-navy: #0a192f;
--color-navy-light: #112240;
--color-slate-lighter: #ccd6f6;
}
Every token in @theme automatically becomes utility classes — bg-primary, text-primary, border-primary — and all support opacity modifiers like bg-primary/20 out of the box.
Step 3: The Automated Sync Script
This is where the magic happens. The script runs as a pre-commit hook so the CSS is always in sync:
- Fetches your Figma file's variable library via the Figma REST API
- Transforms variable names to CSS custom property format
- Regenerates the
@themeblock in your CSS file automatically - Runs as a GitHub Action on every Figma library publish event
// sync-tokens.js
import Figma from 'figma-api';
import fs from 'fs/promises';
const api = new Figma.Api({ personalAccessToken: process.env.FIGMA_TOKEN });
async function syncTokens() {
const file = await api.getFile(process.env.FIGMA_FILE_ID);
const variables = extractVariables(file.document);
const cssVars = variables
.map(({ name, value }) =>
` --color-${name.replace(/\//g, '-')}: ${value};`
)
.join('\n');
const css = await fs.readFile('src/app.css', 'utf-8');
const updated = css.replace(/@theme \{[\s\S]*?\}/, `@theme {\n${cssVars}\n}`);
await fs.writeFile('src/app.css', updated);
console.log(`✓ Synced ${variables.length} tokens from Figma`);
}
syncTokens();
Results After 14 Weeks
- Zero design-to-dev drift at launch — verified with automated pixel-comparison screenshots
- Design iteration time cut by ~60% — no manual token updates, no Zeplin exports
- New engineers onboarded in under 2 hours — the system is completely self-documenting
- 98 Lighthouse performance score maintained — atomic CSS kept bundle minimal
The full sync script and Figma variable template are open-sourced on GitHub. If you have questions, drop a comment below or reach out directly.
S. Saif
Lead Engineer · Full-Stack Developer
Software engineer specialising in fast, accessible, and delightful web products. 5+ years shipping production software. Currently building at Rocket Solutions Limited.
Comments
14Leave a Comment
Comment submitted! It'll appear after a brief review. Thanks for contributing 🙌
This is exactly the workflow we adopted for our last product. The Figma API integration was a bit tricky — the variable scoping in the response JSON isn't the most intuitive — but once you get it, the sync is completely seamless. We've been running it for 3 months with zero drift issues.
Pro tip: set up a GitHub Action that runs the sync script on every Figma webhook
LIBRARY_PUBLISHevent. That way the CSS is always up to date even if a developer forgets to run it manually.The semantic layer is the most important concept in this entire post and I don't think it gets enough emphasis in other articles on design tokens. I've been burned by projects that went straight from raw values to components — every brand refresh becomes a nightmare.
If you're doing any client project with even a 10% chance of a rebrand, this pattern will save you 40+ hours. Speaking from painful experience.
Quick question — are you using the REST API directly or the Figma Plugin API? We've tried building something similar but the REST API rate limits are aggressive with large files (hundreds of variables). Any tips on batching or caching the calls?
Also, do you handle
@mediatoken overrides (different spacing at different breakpoints)? Figma Variables don't natively support breakpoint-aware tokens and that's been our gap.Alternative approach worth mentioning: Style Dictionary by Amazon. It handles multi-platform output — iOS (Swift), Android (XML), and CSS from the same JSON source. If your product has native apps, it's worth the extra setup. Though for web-only the custom script is leaner.