Design che arrivano in produzione come codice che gira, non come ipotesi su un thread Slack
Figma Variables sincronizzate ai token CSS via pipeline tipizzata, ogni componente con un contratto React che rispecchia l'API del componente Figma, auto-layout tradotto in flex/grid senza aritmetica di padding, e una suite di visual regression che fa fallire la PR quando il rendering diverge dal design. Il team smette di discutere se il bottone è 12 o 14 pixel perché il token è la fonte.
Il problema
Il gap di handoff trasforma le decisioni di design in ipotesi degli sviluppatori
Il pattern che fa perdere intent di design è questo. Un designer consegna un file Figma con 200 frame, una colonna di varianti e una palette di colori che ha richiesto due settimane di lavoro. Lo sviluppatore apre il file, ispeziona un frame, vede `padding: 14px 18px 14px 18px` su un bottone e `padding: 16px 20px 16px 20px` su un bottone quasi identico due schermate dopo, e ne sceglie uno. Moltiplica quella decisione su 200 componenti e il prodotto renderizzato non è più il prodotto progettato. Chiudiamo il buco trattando il file Figma come un contratto: le Variables diventano token tipizzati che il codice importa direttamente, ogni componente nominato diventa un componente React con un'API di props che il designer riconosce, l'auto-layout fa mapping in flex senza aritmetica, e una suite di visual regression fa fallire la build quando l'output renderizzato diverge. Il team smette di discutere se lo spacing è 12 o 14 perché design e codice leggono dalla stessa fonte.
Come lo affrontiamo
I sette passi di un handoff Figma→produzione che tiene
L'ordine è fisso. Audit prima della sync, sync prima della spec, spec prima della build, build prima della regression. Saltare l'audit significa ereditare le domande aperte del designer come bug dello sviluppatore.
- 01
Audit del file Figma
Si apre il file e si risponde a sei domande. Colori, spacing, tipografia e radius sono in Figma Variables o sono hex grezzi sparsi tra i componenti? I componenti sono fatti con auto-layout o posizionati a mano? Le varianti sono esaustive (ogni stato, ogni size) o parziali? I nomi dei componenti sono pronti per produzione o ancora battezzati come il file in cui era il designer (`Frame 12`, `Group Copy 7`)? Gli stili sono applicati in modo consistente o sovrascritti per istanza? L'audit produce un CSV con una riga per domanda per componente; tutto il resto della migrazione dipende da lì.
- 02
Sincronizzare Figma Variables con i token CSS
Si usa la REST API Variables di Figma (o un plugin pubblicato) per esportare ogni variabile come JSON. Una pipeline tipizzata trasforma il JSON in custom property CSS `--ds-*` e un export TypeScript dei token. La pipeline gira in CI; la prossima volta che un designer pubblica un cambio di variabile, i token atterrano in una PR. Il nome della variabile in Figma è il nome del token nel codice, uno-a-uno.
- 03
Costruire il contratto di componente
Per ogni componente Figma con nome, si scrive una spec di una pagina. L'header elenca le props (variant, size, state, content), l'API rispecchia ciò che il designer vede nel pannello destro di Figma, e la spec include uno screenshot di ogni variante. Le spec vivono come MDX nel repo accanto al codice del componente; gli sviluppatori non negoziano l'API da un thread di commenti Figma.
- 04
Tradurre auto-layout in flex / grid
I container auto-layout fanno mapping in modo deterministico: `auto-layout horizontal` → flex row, `auto-layout vertical` → flex column, `space between` → `justify-content: space-between`, `hug contents` → `width: max-content`. Il mapping è documentato e lo sviluppatore lo segue senza calcoli. I valori di padding vengono da token `--ds-space-*`, mai dall'ispezione del frame Figma.
- 05
Implementare nel design system
Ogni componente atterra nel design system o nello strato BEM del consumer, a seconda che il pattern si ripeta cross-progetto. L'implementazione usa token per ogni valore (niente hex, niente pixel, niente font-weight letterali). La prima review confronta il componente renderizzato col frame Figma fianco a fianco; la seconda review è una peer code review del contratto React.
- 06
Cablare la visual regression
Una suite Playwright o Chromatic renderizza ogni componente in ogni variante e fa diff contro un'immagine golden. La PR che introduce o modifica un componente deve mostrare un diff visivo verde oppure includere un golden aggiornato con una spiegazione. La divergenza tra file Figma e codice renderizzato smette di essere intercettata per caso in QA e viene presa al momento della PR.
- 07
Documentare l'handoff per il lavoro continuativo
Un README di handoff descrive come un nuovo cambio di design si muove da Figma a produzione. Il designer pubblica il cambio su un branch nominato in Figma; lo sviluppatore tira il diff delle variabili; i componenti coinvolti vengono ri-spec'ati se l'API è cambiata; la PR esce con visual regression aggiornata. L'handoff è un workflow, non un thread di chat; il prossimo cambio non richiede di ri-imparare il processo.
Cosa consegniamo
Report di audit Figma
Un CSV con una riga per componente che risponde alle sei domande di audit (variables, auto-layout, varianti, naming, consistenza, accessibilità). Il manufatto da cui ogni passo successivo dipende; fa emergere il debito di design prima che la build se lo erediti.
Pipeline Figma Variables → token CSS
Uno script Node (o GitHub Action) che tira giù la REST API Variables di Figma, trasforma il JSON in custom property CSS `--ds-*` e un export TypeScript, e apre una PR sui cambi di variabile. Ri-eseguibile; idempotente.
Tabella di riferimento token
Una tabella markdown che mappa ogni Figma Variable al nome del token CSS, al tipo e alla catena di alias Figma. Il riferimento che entrambi i team aprono quando un nome di token salta fuori in review.
Contratto di componente per ogni componente
Una spec MDX per ogni componente Figma, che elenca props (`variant`, `size`, `state`, content), stati, requisiti di accessibilità e thumbnail dei frame Figma incorporati. La fonte di verità unica per cosa fa il componente.
Documento di mapping auto-layout → flex / grid
Un riferimento di una pagina che spiega come ogni impostazione di auto-layout Figma fa mapping in una regola flex o grid CSS, con esempi dal codice reale. Gli sviluppatori smettono di tradurre a mente; il documento è la traduzione.
Implementazione React
Ogni componente costruito con token DS, props tipizzate, export nominati, JSDoc sull'API pubblica. Niente stili inline, niente pixel hardcoded, niente font-weight letterali. L'implementazione corrisponde al contratto, il contratto corrisponde al componente Figma.
Voce Storybook per ogni componente
Una story Storybook per ogni variante, con i control vincolati alle props tipizzate. I designer aprono Storybook contro la URL di staging in review e cliccano tra le varianti senza chiedere a uno sviluppatore di rideployare.
Suite di visual regression
Playwright (o Chromatic) renderizza ogni componente in ogni variante contro uno screenshot baseline. La CI fa fallire la PR quando il diff supera una soglia piccola per componente. La divergenza visiva diventa un errore di build, non una sorpresa al giorno della release.
Workflow Figma Dev Mode
Un walkthrough breve registrato su loom che mostra come gli sviluppatori usano Figma Dev Mode per ispezionare token, copiare CSS per pezzi non-DS e linkare direttamente alla spec del componente nel repo. Il workflow che sopravvive al designer in ferie.
Checklist di accessibilità per componente
Navigazione da tastiera, focus state, contrasto (verificato contro WCAG 2.1 AA), label per screen reader, HTML semantico. Una checklist piccola per componente; la build non passa senza.
Documentazione di handoff
Un README nel repo del design system che descrive il workflow Figma→codice. Dove vivono i design, come si propagano i cambi di variabile, come parte un nuovo componente, come si fa deprecation. Il documento che il prossimo sviluppatore legge il primo giorno.
Processo di design review continuativo
Una review sincrona settimanale di 30 minuti in cui designer e lead developer ripassano i componenti modificati nella settimana. La review è il momento in cui la divergenza viene presa presto; sostituisce il thread asincrono di commenti Figma che nessuno smaltisce.
Cinque file che trasformano un file Figma in un contratto di componente tipizzato
I cinque file qui sotto compongono una pipeline Figma→produzione. L'exporter di variabili che chiama la REST API Figma, il trasformatore di token che scrive custom property CSS e tipi TypeScript, l'MDX del contratto di componente, l'implementazione React che legge da quei token, e il test di visual regression che tiene la linea.
Una migrazione Figma→produzione è un problema di contratto prima che un problema di CSS. Il file Figma esprime intent di design; il codice esprime comportamento a runtime; il buco tra i due è dove vivono i bug. Lo chiudiamo facendo leggere agli stessi dati entrambi: le Figma Variables diventano i token CSS, i componenti Figma diventano i componenti React, e la visual regression tiene la linea dove l'intuito altrimenti deriverebbe.
I cinque file qui sotto compongono una pipeline che trasforma un file Figma ben audited in una libreria di componenti tipizzata. L'exporter di variabili, il trasformatore di token, il contratto di componente, l'implementazione React, e il test di visual regression.
1. L'exporter Figma Variables
Figma espone le Variables su GET /v1/files/:file_key/variables/local. L'exporter si autentica con un personal access token (o, su team plan, uno emesso via OAuth), tira giù ogni collezione di variabili e modo, e scrive un JSON piatto su disco. Il trasformatore a valle consuma quel 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. Il trasformatore di token
Il trasformatore gira il JSON Figma in due manufatti: un file CSS con custom property --ds-* (una regola per modo, light e dark), e un modulo TypeScript che riesporta gli stessi valori. Il CSS è ciò che il runtime legge; il TypeScript è ciò che la JSX importa quando un token deve apparire in logica inline.
// 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. Il contratto di componente
Ogni componente Figma vive in MDX accanto al file React. Il contratto è la fonte di verità: designer e sviluppatore lo leggono entrambi e dissentono esplicitamente in code review della PR, non implicitamente tramite ispezione.
{/* src/components/Button/Button.spec.mdx */}
---
component: Button
figma: https://figma.com/file/abc/Adamarant?node-id=12:34
---
# Button
Elemento interattivo primario. Renderizza un `<button>` semanticamente;
passare `as="a"` per renderizzare un anchor mantenendo l'API visiva identica.
## Props
| Prop | Type | Default | Note |
|-----------|-----------------------------------------------|-----------|------|
| `variant` | `'primary' \| 'ghost' \| 'danger'` | `primary` | Mappa la Figma Variant `Variant=primary/ghost/danger` |
| `size` | `'sm' \| 'md' \| 'lg'` | `md` | Mappa la Figma Variant `Size=sm/md/lg` |
| `state` | `'default' \| 'hover' \| 'active' \| 'disabled'` | derivato | CSS-driven; il frame Figma `State=hover` è solo per documentazione |
| `as` | `'button' \| 'a'` | `button` | Quando `'a'`, richiede `href` |
| `loading` | `boolean` | `false` | Sostituisce label con spinner; disabilita il click |
## Accessibilità
- `aria-busy` impostato quando `loading`
- Focus ring usa `--ds-focus-ring`, mai sovrascritto per progetto
- Target touch ≥ 44 px su `size=sm` via padding
## Riferimento visivo
Embedded da Figma: la matrice di varianti mostra primary/ghost/danger × sm/md/lg = 9 frame.
4. L'implementazione React
Il componente legge i token dal CSS; nessun valore è hardcoded. Le props rispecchiano l'API Figma; lo sviluppatore può rispondere a "com'è Variant=ghost?" leggendo il codice, non il 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. Il test di visual regression
Playwright renderizza ogni variante contro una baseline conservata. Il test fallisce quando il diff supera la soglia per componente. La soglia è piccola (3-5 pixel tipici) e vive nel file di test; alzarla è una conversazione di PR, non un cambio silenzioso.
// 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. Come si tiene insieme tutto questo
L'exporter tira giù le Figma Variables. Il trasformatore scrive token CSS ed export TypeScript. Il contratto nomina ogni componente e le sue props. L'implementazione legge i token, non valori hex. La suite di visual regression intercetta la divergenza prima che esca.
Il file Figma smette di essere un documento di discussione e diventa un contratto che il codebase onora. I designer vedono le loro decisioni arrivare in produzione; gli sviluppatori smettono di tirare a indovinare i valori di padding. L'handoff sparisce come riunione di stato perché l'handoff ora è una pull request: un diff di token, un aggiornamento di contratto, un delta di visual regression. Il prossimo cambio di design si muove nella stessa pipeline; il progetto non paga la tassa di handoff due volte.
Domande frequenti
Vi serve accesso completo al file Figma, o potete lavorare da screenshot?
Accesso completo al file, con permesso di leggere Variables e Components. La pipeline è data-driven; tira giù informazione strutturata che gli screenshot semplicemente non portano. L'accesso read-only via guest seat di organizzazione basta; non ci servono diritti di editing a meno che l'audit riveli fix che il designer ci chiede di applicare direttamente.
E se il nostro file Figma è disordinato (niente Variables, niente auto-layout, componenti inconsistenti)?
Il passo di audit esiste esattamente per questo caso. Cataloghiamo cosa manca e proponiamo due strade: o sistemiamo prima il file Figma (un pre-step di una-due settimane a cui il designer può partecipare) e poi gira la pipeline standard, o costruiamo da screenshot e valori dell'inspector con la consapevolezza esplicita che la visual regression intercetta la divergenza ma nessuna automazione lo farà. La maggior parte dei team preferisce la prima strada perché il file poi continua a produrre valore.
Come gestite i cambi di design dopo la spedizione della build?
Attraverso la stessa pipeline che ha prodotto la prima build. Il designer pubblica un cambio di Variable in Figma; la pipeline di sync apre una PR col diff dei token; i componenti coinvolti vengono individuati in automatico perché referenziano il token; la visual regression segnala il cambio di rendering; la PR esce. Un cambio di tipografia o colore che richiedeva una settimana di inseguimento da parte dello sviluppatore diventa una PR da un giorno.
Pixel-perfect o abbastanza vicino?
Abbastanza vicino dove il pixel-perfect costringerebbe lo sviluppatore a fare override del design system, pixel-perfect dove il design system lo supporta. Il sistema di token codifica le regole del "abbastanza vicino": 8 pixel di padding vivono in `--ds-space-2`, non in un valore one-off. Se il designer specifica 9 pixel da qualche parte, la conversazione è sul perché quel componente devia; la risposta di solito è che 8 era corretto fin dall'inizio.
Sostituite il nostro designer?
No. La pipeline rende il lavoro del designer load-bearing invece che advisory. Il designer resta la fonte delle decisioni di design; il codice smette di essere una traduzione con perdita. I team di solito trovano il designer più contento dopo la migrazione perché il suo file Figma ora è produzione, non un documento di discussione che gli sviluppatori leggono a metà.
Quanto ci vuole?
L'audit prende una settimana. La pipeline di sync delle Variable prende una settimana. Fare spec e build di una libreria di componenti tipica da 30 a 50 componenti prende quattro-sei settimane a seconda della densità (varianti, stati, comportamento responsive). Visual regression e workflow continuativo prendono una settimana. Otto-dieci settimane totali per una libreria di componenti vera; più rapido se il file di design è già disciplinato.
Richiedete Storybook, o possiamo usare qualcos'altro?
Storybook è il default perché il pattern di control + varianti rispecchia il pannello destro Figma del designer uno-a-uno. Alternative come Ladle (più veloce, più leggero) o un playground custom in Next.js funzionano quando c'è una ragione. Il non-negoziabile è che i designer possono aprire una URL deployata e cliccare tra le varianti senza che uno sviluppatore rideployi.
E se non abbiamo un design system in cui mettere i componenti?
Costruirne uno è parte dello scope. La migrazione Figma→produzione è il momento in cui un vero design system parte: i token vengono da Figma Variables, i componenti hanno API tipizzate e checklist di accessibilità, l'implementazione legge da una fonte di token unica. Sei mesi dopo, il design system è l'asset che il team non aveva quando ha cominciato, e sito marketing, UI prodotto e admin lo condividono tutti.
Pianifica l'handoff Figma→produzione
Una call di scoping, un audit del file Figma nella prima settimana, un numero che non cambiamo dopo l'accordo sullo scope. Otto-dieci settimane da kickoff a una libreria di componenti che design ed engineering condividono.