Caso de uso · Figma a producción

Diseños que salen a producción como código que corre, no como conjeturas en un hilo de Slack

Figma Variables sincronizadas a tokens CSS vía una pipeline tipada, cada componente con un contrato React que espeja la API del componente de Figma, auto-layout traducido a flex/grid sin aritmética de padding, y una suite de visual regression que tumba la PR cuando el render diverge del diseño. El equipo deja de discutir si el botón es de 12 o 14 píxeles porque el token es la fuente.

El problema

El gap de handoff convierte decisiones de diseño en conjeturas del desarrollador

El patrón que pierde intent de diseño es este. Un diseñador entrega un archivo Figma con 200 frames, una columna de variantes y una paleta de colores que costó dos semanas refinar. El desarrollador abre el archivo, inspecciona un frame, ve `padding: 14px 18px 14px 18px` en un botón y `padding: 16px 20px 16px 20px` en un botón casi idéntico dos pantallas más allá, y elige uno. Multiplica esa decisión por 200 componentes y el producto renderizado deja de ser el producto diseñado. Cerramos el hueco tratando el archivo de Figma como un contrato: las Variables pasan a ser tokens tipados que el código importa directamente, cada componente con nombre pasa a ser un componente React con una API de props que el diseñador reconoce, el auto-layout mapea a flex sin aritmética, y una suite de visual regression tumba la build cuando el render diverge. El equipo deja de debatir si el espaciado es 12 o 14 porque diseño y código leen de la misma fuente.

Cómo lo abordamos

Los siete pasos de un handoff Figma→producción que aguanta

El orden es fijo. Audit antes del sync, sync antes del spec, spec antes de la build, build antes de la regression. Saltarse el audit significa heredar las preguntas abiertas del diseñador como bugs del desarrollador.

  1. 01

    Auditar el archivo de Figma

    Se abre el archivo y se responden seis preguntas. ¿Los colores, espaciado, tipografía y radius están en Figma Variables o son hex en bruto repartidos por los componentes? ¿Los componentes están hechos con auto-layout o posicionados a mano? ¿Las variantes son exhaustivas (cada estado, cada size) o parciales? ¿Los nombres de los componentes están listos para producción o aún bautizados como el archivo en el que estaba el diseñador (`Frame 12`, `Group Copy 7`)? ¿Los estilos se aplican consistentemente o se sobrescriben por instancia? El audit produce un CSV con una fila por pregunta por componente; todo lo demás de la migración depende de ahí.

  2. 02

    Sincronizar Figma Variables a tokens CSS

    Se usa la REST API de Variables de Figma (o un plugin publicado) para exportar cada variable como JSON. Una pipeline tipada transforma el JSON en custom properties CSS `--ds-*` y un export TypeScript de tokens. La pipeline corre en CI; la próxima vez que un diseñador publique un cambio de variable, los tokens aterrizan en una PR. El nombre de la variable en Figma es el nombre del token en código, uno-a-uno.

  3. 03

    Construir el contrato de componente

    Para cada componente Figma con nombre, se escribe un spec de una página. La cabecera lista las props (variant, size, state, content), la API espeja lo que el diseñador ve en el panel derecho de Figma, y el spec incrusta un screenshot de cada variante. Los specs viven como MDX en el repo al lado del código del componente; los desarrolladores no negocian la API desde un hilo de comentarios de Figma.

  4. 04

    Traducir auto-layout a flex / grid

    Los contenedores auto-layout mapean de forma determinista: `auto-layout horizontal` → flex row, `auto-layout vertical` → flex column, `space between` → `justify-content: space-between`, `hug contents` → `width: max-content`. El mapeo está documentado y el desarrollador lo sigue sin aritmética. Los valores de padding salen de tokens `--ds-space-*`, nunca de inspeccionar el frame de Figma.

  5. 05

    Implementar en el design system

    Cada componente aterriza en el design system o en la capa BEM del consumer, según si el patrón se repite cross-proyecto. La implementación usa tokens para cada valor (sin hex, sin píxeles, sin font-weight literales). La primera review compara el componente renderizado contra el frame de Figma uno al lado del otro; la segunda review es una peer code review del contrato React.

  6. 06

    Cablear la visual regression

    Una suite de Playwright o Chromatic renderiza cada componente en cada variante y diferencia contra una imagen golden. La PR que introduce o cambia un componente tiene que mostrar un diff visual verde o incluir un golden actualizado con explicación. La divergencia entre el archivo de Figma y el código renderizado deja de detectarse por casualidad en QA y se atrapa en el momento de la PR.

  7. 07

    Documentar el handoff para el trabajo continuo

    Un README de handoff describe cómo un cambio de diseño nuevo se mueve de Figma a producción. El diseñador publica el cambio en un branch con nombre en Figma; el desarrollador tira el diff de variables; los componentes afectados se vuelven a spec si la API cambió; la PR sale con visual regression actualizada. El handoff es un workflow, no un hilo de chat; el siguiente cambio no requiere reaprender el proceso.

Qué entregamos

Informe de audit de Figma

Un CSV con una fila por componente que responde a las seis preguntas del audit (variables, auto-layout, variantes, naming, consistencia, accesibilidad). El artefacto del que depende cada paso posterior; saca a la luz la deuda de diseño antes de que la build se la herede.

Pipeline Figma Variables → tokens CSS

Un script Node (o GitHub Action) que tira de la REST API de Variables de Figma, transforma el JSON a custom properties CSS `--ds-*` y un export TypeScript, y abre una PR ante cambios de variable. Re-ejecutable; idempotente.

Tabla de referencia de tokens

Una tabla markdown que mapea cada Figma Variable a su nombre de token CSS, su tipo y la cadena de alias en Figma. La referencia que ambos equipos abren cuando un nombre de token sale en review.

Contrato de componente por cada componente

Un spec MDX por cada componente de Figma, listando props (`variant`, `size`, `state`, content), estados, requisitos de accesibilidad y miniaturas de frames de Figma embebidas. La única fuente de verdad sobre lo que hace el componente.

Documento de mapeo auto-layout → flex / grid

Una referencia de una página que explica cómo cada ajuste de auto-layout de Figma mapea a una regla flex o grid CSS, con ejemplos del propio código. Los desarrolladores dejan de traducir mentalmente; el documento es la traducción.

Implementación React

Cada componente construido con tokens DS, props tipadas, exports con nombre, JSDoc en la API pública. Sin estilos inline, sin píxeles hardcoded, sin font-weight literales. La implementación coincide con el contrato, el contrato coincide con el componente de Figma.

Entrada de Storybook por componente

Una story de Storybook por variante, con los controls atados a las props tipadas. Los diseñadores abren Storybook contra la URL de staging durante la review y hacen clic entre variantes sin pedirle a un desarrollador que vuelva a desplegar.

Suite de visual regression

Playwright (o Chromatic) renderiza cada componente en cada variante contra un screenshot baseline. La CI tumba la PR cuando el diff excede un umbral pequeño por componente. La divergencia visual pasa a ser un error de build, no una sorpresa el día de release.

Workflow de Figma Dev Mode

Un walkthrough corto grabado en loom que muestra cómo los desarrolladores usan Figma Dev Mode para inspeccionar tokens, copiar CSS para piezas no-DS y enlazar directamente al spec del componente en el repo. El workflow que sobrevive al diseñador estando de vacaciones.

Checklist de accesibilidad por componente

Navegación por teclado, focus state, contraste (verificado contra WCAG 2.1 AA), labels para screen reader, HTML semántico. Una checklist pequeña por componente; la build no pasa sin ella.

Documentación de handoff

Un README en el repo del design system que describe el workflow Figma→código. Dónde viven los diseños, cómo se propagan los cambios de variable, cómo arranca un componente nuevo, cómo se hace deprecation. El documento que lee el siguiente desarrollador en su primer día.

Proceso de design review continuo

Una review sincrónica semanal de 30 minutos en la que el diseñador y el lead developer repasan los componentes modificados esa semana. La review es el momento en que la divergencia se atrapa pronto; reemplaza el hilo asíncrono de comentarios de Figma que nadie tramita.

Cinco archivos que convierten un archivo de Figma en un contrato de componente tipado

Los cinco archivos de abajo componen una pipeline Figma→producción. El exporter de variables que llama a la REST API de Figma, el transformador de tokens que escribe custom properties CSS y tipos TypeScript, el MDX del contrato de componente, la implementación React que lee de esos tokens, y el test de visual regression que mantiene la línea.

Una migración Figma→producción es un problema de contrato antes que un problema de CSS. El archivo de Figma expresa intent de diseño; el código expresa comportamiento en runtime; el hueco entre los dos es donde viven los bugs. Lo cerramos haciendo que los mismos datos alimenten ambos: las Figma Variables pasan a ser los tokens CSS, los componentes de Figma pasan a ser los componentes React, y la visual regression mantiene la línea donde la intuición de otro modo derivaría.

Los cinco archivos de abajo componen una pipeline que convierte un archivo de Figma bien auditado en una librería de componentes tipada. El exporter de variables, el transformador de tokens, el contrato de componente, la implementación React, y el test de visual regression.

1. El exporter de Figma Variables

Figma expone las Variables en GET /v1/files/:file_key/variables/local. El exporter se autentica con un personal access token (o, en plan team, uno emitido por OAuth), tira de cada colección de variables y cada modo, y escribe un JSON plano en disco. El transformador aguas abajo consume ese 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. El transformador de tokens

El transformador convierte el JSON de Figma en dos artefactos: un archivo CSS con custom properties --ds-* (una regla por modo, light y dark), y un módulo TypeScript que re-exporta los mismos valores. El CSS es lo que el runtime lee; el TypeScript es lo que importa el JSX cuando un token tiene que aparecer en lógica 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. El contrato de componente

Cada componente Figma vive en MDX al lado del archivo React. El contrato es la fuente de verdad: diseñador y desarrollador lo leen y discrepan explícitamente en la code review de la PR, no implícitamente por inspección.

{/* src/components/Button/Button.spec.mdx */}
---
component: Button
figma: https://figma.com/file/abc/Adamarant?node-id=12:34
---

# Button

Elemento interactivo primario. Renderiza un `<button>` semánticamente;
pasar `as="a"` para renderizar un anchor manteniendo la API visual idéntica.

## Props

| Prop      | Type                                          | Default   | Notas |
|-----------|-----------------------------------------------|-----------|-------|
| `variant` | `'primary' \| 'ghost' \| 'danger'`            | `primary` | Mapea la Figma Variant `Variant=primary/ghost/danger` |
| `size`    | `'sm' \| 'md' \| 'lg'`                        | `md`      | Mapea la Figma Variant `Size=sm/md/lg` |
| `state`   | `'default' \| 'hover' \| 'active' \| 'disabled'` | derivado | CSS-driven; el frame Figma `State=hover` es solo para documentación |
| `as`      | `'button' \| 'a'`                             | `button`  | Cuando es `'a'`, requiere `href` |
| `loading` | `boolean`                                     | `false`   | Reemplaza el label por un spinner; deshabilita el click |

## Accesibilidad

- `aria-busy` puesto cuando `loading`
- Focus ring usa `--ds-focus-ring`, nunca sobrescrito por proyecto
- Target táctil ≥ 44 px en `size=sm` vía padding

## Referencia visual

Embebido desde Figma: la matriz de variantes muestra primary/ghost/danger × sm/md/lg = 9 frames.

4. La implementación React

El componente lee los tokens del CSS; ningún valor está hardcoded. Las props espejan la API de Figma; el desarrollador puede responder "¿cómo se ve Variant=ghost?" leyendo el código, no el archivo.

// 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. El test de visual regression

Playwright renderiza cada variante contra un baseline almacenado. El test falla cuando el diff supera el umbral por componente. El umbral es pequeño (típicamente 3 a 5 píxeles) y vive en el archivo de test; subirlo es una conversación de PR, no un cambio silencioso.

// 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. Cómo encaja todo esto

El exporter tira las Figma Variables. El transformador escribe tokens CSS y exports TypeScript. El contrato pone nombre a cada componente y sus props. La implementación lee tokens, no valores hex. La suite de visual regression atrapa la divergencia antes de que salga.

El archivo de Figma deja de ser un documento de discusión y pasa a ser un contrato que el codebase honra. Los diseñadores ven sus decisiones llegar a producción; los desarrolladores dejan de adivinar valores de padding. El handoff desaparece como reunión de estado porque ahora el handoff es una pull request: un diff de tokens, una actualización de contrato, un delta de visual regression. El siguiente cambio de diseño se mueve por la misma pipeline; el proyecto no paga el impuesto de handoff dos veces.

Stacks relacionados

Preguntas frecuentes

¿Necesitáis acceso completo a nuestro archivo de Figma, o podéis trabajar desde screenshots?

Acceso completo al archivo, con permiso de leer Variables y Components. La pipeline es data-driven; tira información estructurada que los screenshots simplemente no llevan. Con acceso de solo lectura a través de un guest seat de organización basta; no necesitamos derechos de edición a menos que el audit revele arreglos que el diseñador nos pida aplicar directamente.

¿Y si nuestro archivo Figma está desordenado (sin Variables, sin auto-layout, componentes inconsistentes)?

El paso de audit existe exactamente para este caso. Catalogamos qué falta y proponemos dos caminos: o arreglamos primero el archivo Figma (un pre-paso de una o dos semanas en el que el diseñador puede participar) y luego se ejecuta la pipeline estándar, o construimos desde screenshots y valores del inspector con la conciencia explícita de que la visual regression atrapa la divergencia pero ninguna automatización lo hará. La mayoría de los equipos prefiere el primer camino porque el archivo entonces sigue produciendo valor.

¿Cómo gestionáis los cambios de diseño después de desplegar la build?

A través de la misma pipeline que produjo la primera build. El diseñador publica un cambio de Variable en Figma; la pipeline de sync abre una PR con el diff de tokens; los componentes afectados se detectan en automático porque referencian el token; la visual regression señala el cambio de render; la PR sale. Un cambio de tipografía o color que solía costar una semana de persecución por parte del desarrollador pasa a ser una PR de un día.

¿Pixel-perfect o lo bastante cerca?

Lo bastante cerca donde pixel-perfect obligaría al desarrollador a hacer override del design system, pixel-perfect donde el design system lo soporta. El sistema de tokens codifica las reglas del "lo bastante cerca": 8 píxeles de padding viven en `--ds-space-2`, no en un valor one-off. Si el diseñador especifica 9 píxeles en algún sitio, la conversación es sobre por qué ese componente se desvía; la respuesta suele ser que 8 era correcto desde el principio.

¿Reemplazáis a nuestro diseñador?

No. La pipeline hace que el trabajo del diseñador sea load-bearing en lugar de advisory. El diseñador sigue siendo la fuente de las decisiones de diseño; el código deja de ser una traducción con pérdida. Los equipos suelen encontrar a su diseñador más contento tras la migración porque su archivo de Figma ahora es producción, no un documento de discusión que los desarrolladores leen a medias.

¿Cuánto tarda?

El audit lleva una semana. La pipeline de sync de Variables lleva una semana. Hacer spec y build de una librería típica de 30 a 50 componentes lleva de cuatro a seis semanas según la densidad (variantes, estados, comportamiento responsive). La visual regression y el workflow continuo llevan una semana. De ocho a diez semanas en total para una librería de componentes real; más rápido si el archivo de diseño ya está disciplinado.

¿Requerís Storybook, o podemos usar otra cosa?

Storybook es el default porque el patrón de controls + variantes espeja el panel derecho de Figma del diseñador uno-a-uno. Alternativas como Ladle (más rápido, más ligero) o un playground custom en Next.js funcionan cuando hay una razón. El no-negociable es que los diseñadores pueden abrir una URL desplegada y hacer clic entre variantes sin que un desarrollador vuelva a desplegar.

¿Y si no tenemos un design system en el que dejar caer los componentes?

Construir uno es parte del scope. La migración Figma→producción es el momento en que un design system de verdad arranca: los tokens vienen de Figma Variables, los componentes tienen APIs tipadas y checklists de accesibilidad, la implementación lee de una única fuente de tokens. Seis meses después, el design system es el activo que el equipo no tenía cuando empezó, y el sitio marketing, la UI de producto y la admin lo comparten todos.

Planifica tu handoff Figma→producción

Una llamada de scoping, un audit del archivo de Figma en la primera semana, un número que no cambiamos una vez acordado el scope. De ocho a diez semanas desde el kickoff hasta una librería de componentes que comparten diseño e ingeniería.