Designs that ship as the code that runs, not as guesses on a Slack thread
Figma Variables synced to CSS tokens via a typed pipeline, each component spec'd with a React contract that mirrors the Figma component API, auto-layout translated to flex/grid without padding arithmetic, and a visual regression suite that fails the PR when the rendered component drifts from the design. The team stops arguing about whether the button is 12 or 14 pixels because the token is the source.
The problem
The handoff gap turns design decisions into developer guesses
The pattern that loses design intent is this. A designer ships a Figma file with 200 frames, a column of variants, and a colour palette that took two weeks to refine. The developer opens the file, inspects a frame, sees `padding: 14px 18px 14px 18px` on one button and `padding: 16px 20px 16px 20px` on a near-identical button two screens later, and picks one. Multiply that decision across 200 components and the rendered product is no longer the designed product. We close the gap by treating the Figma file as a contract: Variables become typed tokens the code imports directly, each named component becomes a React component with a props API the designer recognises, auto-layout maps to flex without arithmetic, and a visual regression suite fails the build when the rendered output drifts. The team stops debating whether the spacing is 12 or 14 because both the design and the code read from the same source.
Our approach
The seven steps of a Figma-to-production handoff that holds
The order is fixed. Audit before sync, sync before spec, spec before build, build before regression. Skipping the audit means inheriting the designer's open questions as developer bugs.
- 01
Audit the Figma file
Open the file and answer six questions. Are colours, spacing, typography and radii in Figma Variables, or are they raw hex codes scattered across components? Are components made with auto-layout or hand-positioned? Are variants exhaustive (every state, every size) or partial? Are component names production-ready or named after the file the designer was in (`Frame 12`, `Group Copy 7`)? Are styles applied consistently or overridden per instance? The audit produces a CSV with one row per question per component; everything else in the migration depends on it.
- 02
Sync Figma Variables to CSS tokens
Use the Figma Variables REST API (or a published plugin) to export every variable as JSON. A typed pipeline transforms the JSON into `--ds-*` CSS custom properties and a TypeScript token export. The pipeline runs in CI; the next time a designer publishes a variable change, the tokens land in a PR. The variable name in Figma is the token name in code, one-to-one.
- 03
Build the component contract
For every named Figma component, write a one-page spec. The header lists the props (variant, size, state, content), the API matches what the designer sees in the Figma right panel, and the spec embeds a screenshot of every variant. Specs live as MDX in the repo next to the component code; developers do not negotiate the API from a Figma comment thread.
- 04
Translate auto-layout to flex / grid
Auto-layout containers map deterministically: `auto-layout horizontal` → flex row, `auto-layout vertical` → flex column, `space between` → `justify-content: space-between`, `hug contents` → `width: max-content`. The mapping is documented and the developer follows it without arithmetic. Padding values come from `--ds-space-*` tokens, never from inspecting the Figma frame.
- 05
Implement in the design system
Each component lands in the design system or the consumer's BEM layer, depending on whether the pattern repeats cross-project. The implementation uses tokens for every value (no hex, no pixels, no font-weight literals). The first review compares the rendered component against the Figma frame side by side; the second review is a peer code review of the React contract.
- 06
Wire visual regression
A Playwright or Chromatic suite renders every component in every variant and diffs against a golden image. The PR that introduces or changes a component must show a green visual diff or include an updated golden with an explanation. Drift between the Figma file and the rendered code stops being detected by chance in QA and gets caught at PR time.
- 07
Document the handoff for ongoing work
A handoff README describes how a new design change moves from Figma to production. The designer publishes the change to a named branch in Figma; the dev pulls the variable diff; the affected components get re-spec'd if the API changed; the PR ships with updated visual regression. The handoff is a workflow, not a chat thread; the next change does not require relearning the process.
What we deliver
Figma audit report
A CSV with one row per component answering the six audit questions (variables, auto-layout, variants, naming, consistency, accessibility). The artefact every later step depends on; surfaces the design debt before the build inherits it.
Figma Variables → CSS token pipeline
A Node script (or GitHub Action) that pulls the Figma Variables REST API, transforms the JSON into `--ds-*` CSS custom properties and a TypeScript export, and opens a PR on variable changes. Re-runnable; idempotent.
Token reference table
A markdown table mapping every Figma Variable to its CSS token name, type, and Figma alias chain. The reference both teams open when a token name comes up in review.
Component contract per component
One MDX spec per Figma component, listing props (`variant`, `size`, `state`, content), states, accessibility requirements, and embedded Figma frame thumbnails. The single source of truth for what the component does.
Auto-layout → flex / grid mapping doc
A one-page reference explaining how every Figma auto-layout setting maps to a flex or grid CSS rule, with examples from the actual codebase. Developers stop translating mentally; the doc is the translation.
React implementation
Every component built with DS tokens, typed props, named exports, JSDoc on the public API. No inline styles, no hardcoded pixels, no font-weight literals. The implementation matches the contract, the contract matches the Figma component.
Storybook entry per component
A Storybook story for every variant, with controls bound to the typed props. Designers open Storybook against the staging URL during review and click through variants without asking a developer to redeploy.
Visual regression suite
Playwright (or Chromatic) renders every component in every variant against a baseline screenshot. The CI fails the PR when the diff exceeds a small per-component threshold. Visual drift becomes a build error, not a release-day surprise.
Figma Dev Mode workflow
A short loom-recorded walkthrough showing how developers use Figma Dev Mode to inspect tokens, copy CSS for non-DS pieces, and link directly to the component spec in the repo. The workflow that survives the designer being on holiday.
Accessibility checklist per component
Keyboard navigation, focus state, contrast (verified against WCAG 2.1 AA), screen-reader labels, semantic HTML. A small per-component checklist; the build does not pass without it.
Handoff documentation
A README in the design system repo that describes the Figma-to-code workflow. Where designs live, how variable changes propagate, how a new component starts, how to deprecate. The doc the next developer reads on day one.
Ongoing design review process
A weekly 30-minute synchronous review where the designer and the lead developer walk through the components changed that week. The review is the moment drift gets caught early; it replaces the asynchronous Figma comment thread that nobody triages.
Five files that turn a Figma file into a typed component contract
The five files below compose a Figma-to-production pipeline. The variable exporter that calls the Figma REST API, the token transformer that writes CSS custom properties and TypeScript types, the component contract MDX, the React implementation reading from those tokens, and the visual regression test that holds the line.
A Figma-to-production migration is a contract problem before it is a CSS problem. The Figma file expresses design intent; the code expresses runtime behaviour; the gap between them is where bugs live. We close the gap by making the same data feed both: Figma Variables become the CSS tokens, Figma components become the React components, and visual regression holds the line where intuition would otherwise drift.
The five files below compose a pipeline that turns a well-audited Figma file into a typed component library. The variable exporter, the token transformer, the component contract, the React implementation, and the visual regression test.
1. The Figma Variables exporter
Figma exposes Variables through GET /v1/files/:file_key/variables/local. The exporter authenticates with a personal access token (or, on a team plan, an OAuth-issued one), pulls every variable collection and mode, and writes a flat JSON to disk. The transformer downstream consumes that JSON.
// scripts/figma/export-variables.ts
import { writeFile } from 'node:fs/promises'
const FIGMA_TOKEN = process.env.FIGMA_TOKEN!
const FILE_KEY = process.env.FIGMA_FILE_KEY!
interface FigmaVariable {
id: string
name: string
resolvedType: 'COLOR' | 'FLOAT' | 'STRING' | 'BOOLEAN'
valuesByMode: Record<string, unknown>
}
interface FigmaVariablesResponse {
meta: {
variables: Record<string, FigmaVariable>
variableCollections: Record<string, { name: string; modes: { modeId: string; name: string }[] }>
}
}
async function main(): Promise<void> {
const res = await fetch(
`https://api.figma.com/v1/files/${FILE_KEY}/variables/local`,
{ headers: { 'X-Figma-Token': FIGMA_TOKEN } },
)
if (!res.ok) throw new Error(`figma: ${res.status}`)
const data = (await res.json()) as FigmaVariablesResponse
await writeFile(
'./data/figma-variables.json',
JSON.stringify(data.meta, null, 2),
'utf8',
)
console.log(
`exported ${Object.keys(data.meta.variables).length} variables across ${Object.keys(data.meta.variableCollections).length} collections`,
)
}
void main()
2. The token transformer
The transformer turns the Figma JSON into two artefacts: a CSS file with --ds-* custom properties (one rule per mode, light and dark), and a TypeScript module that re-exports the same values. The CSS is what the runtime reads; the TypeScript is what JSX imports when a token must appear in inline logic.
// scripts/figma/transform-tokens.ts
import { readFile, writeFile } from 'node:fs/promises'
interface VariableCollection {
name: string
modes: { modeId: string; name: string }[]
}
interface VariablesMeta {
variables: Record<string, {
name: string
resolvedType: 'COLOR' | 'FLOAT' | 'STRING' | 'BOOLEAN'
variableCollectionId: string
valuesByMode: Record<string, { r: number; g: number; b: number; a: number } | number | string | boolean>
}>
variableCollections: Record<string, VariableCollection>
}
function rgbaToHex(c: { r: number; g: number; b: number; a: number }): string {
const to255 = (n: number): number => Math.round(n * 255)
const hex = (n: number): string => n.toString(16).padStart(2, '0')
if (c.a < 1) return `rgba(${to255(c.r)}, ${to255(c.g)}, ${to255(c.b)}, ${c.a.toFixed(3)})`
return `#${hex(to255(c.r))}${hex(to255(c.g))}${hex(to255(c.b))}`
}
function toKebab(name: string): string {
return name
.replace(/\//g, '-')
.replace(/([a-z])([A-Z])/g, '$1-$2')
.toLowerCase()
}
async function main(): Promise<void> {
const meta = JSON.parse(await readFile('./data/figma-variables.json', 'utf8')) as VariablesMeta
const cssLines: string[] = []
for (const collection of Object.values(meta.variableCollections)) {
for (const mode of collection.modes) {
const selector = mode.name.toLowerCase() === 'dark' ? '[data-theme="dark"]' : ':root'
cssLines.push(`${selector} {`)
for (const v of Object.values(meta.variables)) {
if (v.variableCollectionId !== Object.entries(meta.variableCollections).find(([, c]) => c === collection)?.[0]) continue
const value = v.valuesByMode[mode.modeId]
if (value === undefined) continue
const tokenName = `--ds-${toKebab(v.name)}`
if (v.resolvedType === 'COLOR' && typeof value === 'object') {
cssLines.push(` ${tokenName}: ${rgbaToHex(value as { r: number; g: number; b: number; a: number })};`)
} else if (v.resolvedType === 'FLOAT' && typeof value === 'number') {
cssLines.push(` ${tokenName}: ${value}px;`)
}
}
cssLines.push('}', '')
}
}
await writeFile('./src/styles/tokens.generated.css', cssLines.join('\n'), 'utf8')
console.log(`wrote tokens.generated.css with ${cssLines.length} lines`)
}
void main()
3. The component contract
Each Figma component lives in MDX next to the React file. The contract is the source of truth: developer and designer both read it and disagree explicitly in PR review, not implicitly through inspection.
{/* src/components/Button/Button.spec.mdx */}
---
component: Button
figma: https://figma.com/file/abc/Adamarant?node-id=12:34
---
# Button
Primary interactive element. Renders a `<button>` semantically; pass
`as="a"` to render an anchor while keeping the visual API identical.
## Props
| Prop | Type | Default | Notes |
|-----------|-----------------------------------------------|-----------|-------|
| `variant` | `'primary' \| 'ghost' \| 'danger'` | `primary` | Maps to Figma Variant `Variant=primary/ghost/danger` |
| `size` | `'sm' \| 'md' \| 'lg'` | `md` | Maps to Figma Variant `Size=sm/md/lg` |
| `state` | `'default' \| 'hover' \| 'active' \| 'disabled'` | derived | CSS-driven; Figma frame `State=hover` is for documentation only |
| `as` | `'button' \| 'a'` | `button` | When `'a'`, requires `href` |
| `loading` | `boolean` | `false` | Replaces label with spinner; disables click |
## Accessibility
- `aria-busy` set when `loading`
- Focus ring uses `--ds-focus-ring`, never overridden per project
- Touch target ≥ 44 px on `size=sm` via padding
## Visual reference
Embedded from Figma: Variant matrix shows primary/ghost/danger × sm/md/lg = 9 frames.
4. The React implementation
The component reads tokens from CSS; no value is hardcoded. Props mirror the Figma API; the developer can answer "what does Variant=ghost look like?" by reading the code, not the file.
// src/components/Button/Button.tsx
import { type ButtonHTMLAttributes, type AnchorHTMLAttributes } from 'react'
import { cn } from '@/lib/utils'
interface BaseProps {
variant?: 'primary' | 'ghost' | 'danger'
size?: 'sm' | 'md' | 'lg'
loading?: boolean
children: React.ReactNode
}
type ButtonProps =
| (BaseProps & { as?: 'button' } & ButtonHTMLAttributes<HTMLButtonElement>)
| (BaseProps & { as: 'a'; href: string } & AnchorHTMLAttributes<HTMLAnchorElement>)
export function Button(props: ButtonProps): React.ReactElement {
const { variant = 'primary', size = 'md', loading = false, children, className, ...rest } = props
const classes = cn(
'ds-btn',
`ds-btn--${variant}`,
`ds-btn--${size}`,
loading && 'ds-btn--loading',
className,
)
if (props.as === 'a') {
return (
<a
className={classes}
aria-busy={loading || undefined}
{...(rest as AnchorHTMLAttributes<HTMLAnchorElement>)}
>
{loading ? <span className="ds-spinner" aria-hidden /> : children}
</a>
)
}
return (
<button
className={classes}
aria-busy={loading || undefined}
disabled={loading || (rest as ButtonHTMLAttributes<HTMLButtonElement>).disabled}
{...(rest as ButtonHTMLAttributes<HTMLButtonElement>)}
>
{loading ? <span className="ds-spinner" aria-hidden /> : children}
</button>
)
}
5. The visual regression test
Playwright renders each variant against a stored baseline. The test fails when the diff exceeds the per-component threshold. The threshold is small (3 to 5 pixels typically) and lives in the test file; raising it is a PR conversation, not a silent change.
// tests/visual/Button.spec.ts
import { test, expect } from '@playwright/test'
const VARIANTS = ['primary', 'ghost', 'danger'] as const
const SIZES = ['sm', 'md', 'lg'] as const
for (const variant of VARIANTS) {
for (const size of SIZES) {
test(`Button variant=${variant} size=${size}`, async ({ page }) => {
await page.goto(`/storybook/?path=/story/button--${variant}-${size}`)
const button = page.locator('[data-testid="story-root"] .ds-btn')
await button.waitFor()
await expect(button).toHaveScreenshot(`button-${variant}-${size}.png`, {
maxDiffPixels: 4,
})
})
}
}
test('Button loading state', async ({ page }) => {
await page.goto('/storybook/?path=/story/button--loading')
const button = page.locator('[data-testid="story-root"] .ds-btn')
await button.waitFor()
await expect(button).toHaveScreenshot('button-loading.png', { maxDiffPixels: 4 })
})
6. What this composes
The exporter pulls Figma Variables. The transformer writes CSS tokens and TypeScript exports. The contract names every component and its props. The implementation reads tokens, not hex values. The visual regression suite catches drift before it ships.
The Figma file stops being a discussion document and becomes a contract the codebase honours. Designers see their decisions arrive in production; developers stop guessing at padding values. The handoff disappears as a status meeting because the handoff is now a pull request: a token diff, a contract update, a visual regression delta. The next design change moves through the same pipeline; the project does not pay the handoff tax twice.
Frequently asked questions
Do you need full access to our Figma file, or can you work from screenshots?
Full access to the file, with permission to read Variables and Components. The pipeline is data-driven; it pulls structured information that screenshots simply do not carry. Read-only access through an organisation guest seat is enough; we do not need editing rights unless the audit reveals fixes the designer asks us to apply directly.
What if our Figma file is messy (no Variables, no auto-layout, inconsistent components)?
The audit step exists for exactly this case. We catalogue what's missing and propose two paths: either we tidy the Figma file first (a one to two week pre-step the designer can join) and then run the standard pipeline, or we build from screenshots and inspector values with the explicit understanding that visual regression catches drift but no automation will. Most teams prefer the first path because the file then keeps producing value.
How do you handle design changes after the build is shipped?
Through the same pipeline that produced the first build. The designer publishes a Variable change in Figma; the sync pipeline opens a PR with the token diff; the affected components are spotted automatically because they reference the token; visual regression flags the rendering change; the PR ships. A typography or colour change that used to take a week of developer chasing becomes a one-day PR.
Pixel-perfect or close-enough?
Close enough where pixel-perfect would force the developer to override the design system, pixel-perfect where the design system supports it. The token system encodes the close-enough rules: 8 pixels of padding lives in `--ds-space-2`, not in a one-off value. If the designer specifies 9 pixels somewhere, the conversation is about why that one component deviates; the answer is usually that 8 was correct all along.
Do you replace our designer?
No. The pipeline makes the designer's work load-bearing instead of advisory. The designer stays the source of design decisions; the code stops being a lossy translation. Teams typically find their designer is happier after the migration because their Figma file is now production, not a discussion document developers half-read.
How long does it take?
The audit takes one week. The Variable sync pipeline takes one week. Spec'ing and building a typical 30 to 50 component library takes four to six weeks depending on density (variants, states, responsive behaviour). Visual regression and the ongoing workflow take one week. Eight to ten weeks total for a real component library; faster if the design file is already disciplined.
Do you require Storybook, or can we use something else?
Storybook is the default because the controls + variants pattern matches the designer's Figma right panel one-to-one. Alternatives like Ladle (faster, lighter) or a custom playground in Next.js work when there's a reason. The non-negotiable is that designers can open a deployed URL and click through variants without a developer redeploying.
What if we don't have a design system to drop the components into?
Building one is part of the scope. The Figma-to-production migration is the moment a real design system gets started: the tokens come from Figma Variables, the components have typed APIs and accessibility checklists, the implementation reads from a single token source. Six months later, the design system is the asset the team did not have when they started, and the marketing site, product UI and admin all share it.
Plan your Figma-to-production handoff
A scoping call, an audit of the Figma file in the first week, a number we will not change once we agree on scope. Eight to ten weeks from kickoff to a component library the design and engineering teams share.