Crear un design system con Claude Code: tokens, Storybook y CI
Crea tokens, componentes React, pruebas de accesibilidad y CI con Claude Code sin perder el control del diseño.
El problema empieza cuando cada pantalla interpreta el diseño a su manera
Imagina que acabas de cambiar el azul principal de una aplicación. El botón de acceso ya usa el color nuevo, pero el formulario de pago conserva el anterior, el estado de foco ha desaparecido y una tarjeta queda ilegible en modo oscuro. El equipo creó componentes, pero no creó una forma segura de cambiar el producto.
Un design system no es solo una galería de botones, tarjetas y formularios. Es un modelo de trabajo para cambiar colores, espacios, tipografía, estados, revisiones y pruebas sin romper pantallas. Claude Code puede leer el repositorio, editar varios archivos relacionados, ejecutar Storybook y las pruebas, y resumir el diff. No decide qué representa la marca ni sustituye la revisión final de accesibilidad.
Lo que te llevarás de esta guía
- Convertir decisiones visuales en tokens que se puedan revisar y generar.
- Construir un componente React/TypeScript con estados explícitos y una API estable.
- Usar Storybook con
@storybook/addon-vitestyvitest --project=storybookcomo ruta recomendada en 2026. - Separar las tareas repetibles que puede ejecutar Claude Code de las decisiones que debe tomar una persona.
La guía recorre tokens, componentes, Storybook, accesibilidad, pruebas visuales y CI. Para profundizar, consulta la gestión de Design Tokens con Claude Code, el desarrollo con Storybook y la accesibilidad con Claude Code.
Arquitectura objetivo: una fuente de verdad que CI pueda comprobar
En este flujo, tokens.json es el contrato del código. Figma sigue siendo esencial para explorar y revisar el diseño, pero sus decisiones deben llegar al repositorio en un formato que admita diff, revisión y validación automática.
flowchart LR
Figma["Figma Variables"]
Tokens["tokens.json"]
Build["token build script"]
CSS["CSS variables"]
TS["TypeScript token map"]
Components["React components"]
Storybook["Storybook stories"]
CI["Visual and a11y CI"]
Figma -->|review input| Tokens
Tokens --> Build
Build --> CSS
Build --> TS
CSS --> Components
TS --> Components
Components --> Storybook
Storybook --> CI
Los Design Tokens son decisiones de diseño guardadas como datos y con nombres estables: colores, espacios, radios, tipografía y estados. Un valor bruto como #2563eb explica su apariencia, pero action.background.primary explica para qué se usa. Esa diferencia permite cambiar una marca sin buscar códigos hexadecimales por todo el proyecto.
Las referencias vigentes son el formato del Design Tokens Community Group, la documentación de Claude Code, su guía de seguridad, el addon Vitest de Storybook, las guías de pruebas de accesibilidad y pruebas visuales, y la API REST de Figma.
Qué puede hacer Claude Code y qué debe decidir una persona
Claude Code funciona mejor cuando conoce el límite de archivos, el comportamiento que debe conservar y los comandos que demostrarán el resultado. «Crea un design system» abre demasiadas decisiones a la vez. «Migra solo Button, conserva su API pública, añade sus estados a Storybook y ejecuta las pruebas de accesibilidad» produce un cambio que una persona puede revisar.
| Área | Alcance adecuado para Claude Code | Decisión humana |
|---|---|---|
| Tokens | Extraer colores y espacios repetidos del CSS | Significado de marca y nombres definitivos |
| Componentes | Implementar primitivas tipadas como Button, Input y Alert | API pública y semántica del producto |
| Storybook | Añadir variantes, estados y stories de interacción | Qué estados representan flujos reales |
| Accesibilidad | Detectar etiquetas ausentes, problemas de foco y avisos de axe | Validación final con teclado y lector de pantalla |
| CI | Añadir controles visuales y de accesibilidad a los pull requests | Política de bloqueo y gestión de excepciones |
Antes de permitir cambios, entrega a Claude Code una regla breve de proyecto:
Reglas para tareas del design system:
- Edita únicamente src/components, src/styles, .storybook, tests, scripts y tokens.json.
- No cambies colores de marca sin enumerar los nombres de token anteriores y nuevos.
- Cada componente nuevo necesita props TypeScript, comportamiento de teclado, stories de Storybook y notas de accesibilidad.
- Ejecuta npm run tokens:build, npm run test:storybook -- --run, npm run build-storybook y npm run test:visual antes de informar que has terminado.
- Si cambia el comportamiento del foco, incluye pasos de revisión manual.
La seguridad forma parte del flujo. No pegues tokens de acceso de Figma o npm, secretos de CI ni capturas privadas de clientes en un prompt. Limita los permisos de Claude Code, revisa cada comando antes de aprobarlo y exige aprobación humana para actualizaciones masivas de snapshots.
Instalación mínima con Storybook y Vitest
El ejemplo parte de una aplicación React con TypeScript y clases de utilidad. Ajusta los comandos al gestor de paquetes del proyecto.
npm install class-variance-authority clsx tailwind-merge
npx storybook@latest init
npx storybook add @storybook/addon-a11y
npx storybook add @storybook/addon-vitest
npm install -D @playwright/test concurrently http-server wait-on
npx playwright install chromium
En 2026, la ruta recomendada para un Storybook basado en Vite es @storybook/addon-vitest. También es compatible con la integración Next.js sobre Vite documentada por Storybook. Si el proyecto no puede usar esos frameworks, consulta el antiguo @storybook/test-runner únicamente como alternativa de migración; no sustituyas el comando de Vitest sin comprobar el framework.
Añade scripts idénticos para desarrollo local y CI. El punto clave es que test:storybook ejecute vitest --project=storybook:
{
"scripts": {
"tokens:build": "node scripts/build-tokens.mjs",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"test:storybook": "vitest --project=storybook",
"test:visual": "playwright test tests/button.visual.spec.ts"
}
}
Convierte los tokens en un contrato verificable
Divide los tokens en tres capas. Los primitivos guardan valores brutos; los semánticos describen intención; los de componente representan estados concretos de la interfaz. La separación evita que un cambio de marca se convierta en una sustitución global difícil de revisar.
{
"$schema": "https://www.designtokens.org/schemas/2025.10/format.json",
"primitive": {
"color": {
"blue": {
"50": {
"$type": "color",
"$value": { "colorSpace": "srgb", "components": [0.9373, 0.9647, 1], "hex": "#eff6ff" }
},
"600": {
"$type": "color",
"$value": { "colorSpace": "srgb", "components": [0.1451, 0.3882, 0.9216], "hex": "#2563eb" }
},
"700": {
"$type": "color",
"$value": { "colorSpace": "srgb", "components": [0.1137, 0.3059, 0.8471], "hex": "#1d4ed8" }
}
},
"gray": {
"50": {
"$type": "color",
"$value": { "colorSpace": "srgb", "components": [0.9765, 0.9804, 0.9843], "hex": "#f9fafb" }
},
"200": {
"$type": "color",
"$value": { "colorSpace": "srgb", "components": [0.898, 0.9059, 0.9216], "hex": "#e5e7eb" }
},
"900": {
"$type": "color",
"$value": { "colorSpace": "srgb", "components": [0.0667, 0.0941, 0.1529], "hex": "#111827" }
}
},
"red": {
"600": {
"$type": "color",
"$value": { "colorSpace": "srgb", "components": [0.8627, 0.149, 0.149], "hex": "#dc2626" }
},
"700": {
"$type": "color",
"$value": { "colorSpace": "srgb", "components": [0.7255, 0.1098, 0.1098], "hex": "#b91c1c" }
}
},
"white": {
"$type": "color",
"$value": { "colorSpace": "srgb", "components": [1, 1, 1], "hex": "#ffffff" }
}
},
"space": {
"2": { "$type": "dimension", "$value": { "value": 0.5, "unit": "rem" } },
"3": { "$type": "dimension", "$value": { "value": 0.75, "unit": "rem" } },
"4": { "$type": "dimension", "$value": { "value": 1, "unit": "rem" } },
"6": { "$type": "dimension", "$value": { "value": 1.5, "unit": "rem" } }
},
"radius": {
"md": { "$type": "dimension", "$value": { "value": 0.375, "unit": "rem" } },
"lg": { "$type": "dimension", "$value": { "value": 0.5, "unit": "rem" } }
}
},
"semantic": {
"color": {
"surface": { "$type": "color", "$value": "{primitive.color.white}" },
"text": { "$type": "color", "$value": "{primitive.color.gray.900}" },
"border": { "$type": "color", "$value": "{primitive.color.gray.200}" },
"focus": { "$type": "color", "$value": "{primitive.color.blue.600}" }
}
},
"component": {
"button": {
"primary": {
"background": { "$type": "color", "$value": "{primitive.color.blue.600}" },
"backgroundHover": { "$type": "color", "$value": "{primitive.color.blue.700}" },
"text": { "$type": "color", "$value": "{primitive.color.white}" }
},
"danger": {
"background": { "$type": "color", "$value": "{primitive.color.red.600}" },
"backgroundHover": { "$type": "color", "$value": "{primitive.color.red.700}" },
"text": { "$type": "color", "$value": "{primitive.color.white}" }
}
}
}
}
Genera variables CSS y un mapa TypeScript desde el mismo archivo. Así, estilos y lógica consumen exactamente los mismos nombres:
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname } from "node:path";
const source = JSON.parse(readFileSync("tokens.json", "utf8"));
function getToken(path) {
const node = path.split(".").reduce((current, key) => current?.[key], source);
if (!node || typeof node.$value === "undefined") {
throw new Error(`Unknown token reference: ${path}`);
}
return node.$value;
}
function resolveValue(value, stack = []) {
if (typeof value === "string" && value.startsWith("{") && value.endsWith("}")) {
const path = value.slice(1, -1);
if (stack.includes(path)) {
throw new Error(`Circular token reference: ${[...stack, path].join(" -> ")}`);
}
return resolveValue(getToken(path), [...stack, path]);
}
return value;
}
function toCssValue(value) {
if (value && typeof value === "object") {
if (typeof value.hex === "string") return value.hex;
if (typeof value.value === "number" && typeof value.unit === "string") {
return `${value.value}${value.unit}`;
}
throw new Error(`Unsupported token value: ${JSON.stringify(value)}`);
}
return String(value);
}
function walk(node, pathParts = [], result = {}) {
if (!node || typeof node !== "object") return result;
if (node && typeof node === "object" && typeof node.$value !== "undefined") {
result[pathParts.join("-")] = toCssValue(resolveValue(node.$value));
return result;
}
for (const [key, value] of Object.entries(node)) {
if (key.startsWith("$")) continue;
walk(value, [...pathParts, key], result);
}
return result;
}
const flat = walk(source);
const css = [
":root {",
...Object.entries(flat).map(([name, value]) => ` --${name}: ${value};`),
"}",
""
].join("\n");
mkdirSync(dirname("src/styles/tokens.css"), { recursive: true });
mkdirSync(dirname("src/tokens.ts"), { recursive: true });
writeFileSync("src/styles/tokens.css", css);
writeFileSync("src/tokens.ts", `export const tokens = ${JSON.stringify(flat, null, 2)} as const;\n`);
console.log(`Generated ${Object.keys(flat).length} tokens.`);
No pidas a Claude Code que reescriba toda la interfaz en el primer paso. Pídele que localice colores y espacios repetidos, los asocie a candidatos de token y entregue un informe antes de editar. Una persona debe aprobar los nombres, porque forman parte del lenguaje compartido entre diseño y desarrollo.
Construye componentes React tipados y previsibles
La capa de componentes debe ser previsible. Este Button declara variantes, tamaños, carga, desactivación y foco visible. No intenta resolver todos los botones futuros: expone una API pequeña que puede evolucionar con pruebas.
import { forwardRef, type ButtonHTMLAttributes } from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
const buttonVariants = cva(
[
"inline-flex items-center justify-center gap-2 rounded-md font-medium",
"transition-colors focus-visible:outline-none focus-visible:ring-2",
"focus-visible:ring-[var(--semantic-color-focus)] focus-visible:ring-offset-2",
"disabled:pointer-events-none disabled:opacity-50"
],
{
variants: {
variant: {
primary: [
"bg-[var(--component-button-primary-background)]",
"text-[var(--component-button-primary-text)]",
"hover:bg-[var(--component-button-primary-backgroundHover)]"
],
secondary: "border border-[var(--semantic-color-border)] bg-[var(--semantic-color-surface)] text-[var(--semantic-color-text)] hover:bg-gray-50",
danger: [
"bg-[var(--component-button-danger-background)]",
"text-[var(--component-button-danger-text)]",
"hover:bg-[var(--component-button-danger-backgroundHover)]"
]
},
size: {
sm: "h-8 px-3 text-sm",
md: "h-10 px-4 text-sm",
lg: "h-12 px-6 text-base"
}
},
defaultVariants: {
variant: "primary",
size: "md"
}
}
);
export interface ButtonProps
extends ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
loading?: boolean;
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
{ className, variant, size, loading = false, disabled, children, ...props },
ref
) {
return (
<button
ref={ref}
className={cn(buttonVariants({ variant, size }), className)}
disabled={disabled || loading}
aria-busy={loading || undefined}
{...props}
>
{loading ? (
<span
aria-hidden="true"
className="h-4 w-4 animate-spin rounded-full border-2 border-current border-r-transparent"
/>
) : null}
<span>{children}</span>
</button>
);
});
La revisión no termina al preguntar si el botón se ve bien. Hay que comprobar si sus props tienen un significado estable, si los estados se pueden usar solo con teclado y si varios equipos podrían actualizarlo sin romper sus pantallas.
Convierte Storybook en una especificación ejecutable
Cada estado relevante debe existir como story. Si carga, error, desactivación o foco no aparecen en Storybook, será difícil revisarlos, probarlos y discutirlos con diseño. El ejemplo conserva los textos en inglés para que el bloque sea idéntico al código verificado en la fuente.
import type { Meta, StoryObj } from "@storybook/react-vite";
import { Button } from "./Button";
const meta = {
title: "Design System/Button",
component: Button,
parameters: {
layout: "centered",
a11y: {
test: "error"
}
},
argTypes: {
variant: {
control: "select",
options: ["primary", "secondary", "danger"]
},
size: {
control: "select",
options: ["sm", "md", "lg"]
},
loading: { control: "boolean" },
disabled: { control: "boolean" }
}
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: {
children: "Save changes",
variant: "primary"
}
};
export const Danger: Story = {
args: {
children: "Delete",
variant: "danger"
}
};
export const Loading: Story = {
args: {
children: "Saving",
loading: true
}
};
export const AllStates: Story = {
render: () => (
<div className="flex flex-wrap items-center gap-3">
<Button variant="primary" size="sm">Small</Button>
<Button variant="primary" size="md">Medium</Button>
<Button variant="primary" size="lg">Large</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="danger">Danger</Button>
<Button disabled>Disabled</Button>
<Button loading>Loading</Button>
</div>
)
};
Indica a Claude Code que conserve las stories existentes, añada solo los estados ausentes y explique cualquier cambio de identificador. Los identificadores afectan a URLs, snapshots e informes, por lo que no deben cambiar como efecto secundario de una refactorización.
Ejecuta componentes, accesibilidad y snapshots en CI
Las pruebas automáticas no sustituyen una revisión con teclado y lector de pantalla, pero detectan pronto muchas infracciones estructurales. Para Storybook basado en Vite, el addon Vitest convierte las stories en pruebas de navegador. Con parameters.a11y.test = "error", una infracción detectada por el addon de accesibilidad hace fallar la prueba del componente.
En CI, ejecuta npm run test:storybook -- --run. A diferencia del flujo antiguo con test-runner, Vitest no necesita servir Storybook por separado para las pruebas de componentes y accesibilidad. El servidor de la compilación estática solo se conserva para el snapshot personalizado de Playwright que aparece a continuación.
Empieza con pocas capturas y elige stories de alto valor. Fechas, animaciones, fuentes remotas e identificadores aleatorios introducen ruido; congela esos datos antes de ampliar la cobertura visual.
Antes de activar la CI, ejecuta una vez npx playwright test tests/button.visual.spec.ts --update-snapshots en la aplicación de destino, revisa la imagen base y añádela al repositorio. Sin esa referencia, la primera ejecución falla porque Playwright no tiene con qué comparar.
import { expect, test } from "@playwright/test";
test("button all states visual snapshot", async ({ page }) => {
await page.goto("http://127.0.0.1:6006/iframe.html?id=design-system-button--all-states");
await expect(page).toHaveScreenshot("button-all-states.png", {
fullPage: true,
animations: "disabled"
});
});
Conecta después los mismos pasos a GitHub Actions:
name: design-system-quality
on:
pull_request:
paths:
- "tokens.json"
- "scripts/build-tokens.mjs"
- "src/components/**"
- "src/styles/**"
- ".storybook/**"
- "tests/**"
- "package.json"
- "package-lock.json"
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run tokens:build
- run: npx playwright install --with-deps chromium
- run: npm run test:storybook -- --run
- run: npm run build-storybook
- run: >
npx concurrently -k -s first -n server,tests
"npx http-server storybook-static -p 6006"
"npx wait-on http://127.0.0.1:6006 && npm run test:visual"
Cuando CI falle, facilita a Claude Code el ID de la story, la infracción de accesibilidad, los archivos modificados y el diff visual. No pegues secretos ni registros completos: recorta la evidencia al error necesario para diagnosticarlo.
Un límite realista para la integración con Figma
Las variables de Figma son una entrada valiosa, pero una sincronización bidireccional automática suele ser arriesgada al principio. Experimentos no aprobados, nombres antiguos o notas privadas pueden terminar en tokens de producción sin una revisión consciente.
| Área | Automatización adecuada | Evita |
|---|---|---|
| Variables de Figma | Exportar y comparar con tokens.json | Sobrescribir tokens de producción a ciegas |
| Componentes de Figma | Recoger candidatos de estado y props | Decidir automáticamente la API de React |
| Comentarios de Figma | Resumir preguntas pendientes | Inferir la intención final del diseño |
| Enlaces de Storybook | Adjuntar stories a la revisión | Tratar Storybook como aprobación de diseño |
Pide primero a Claude Code un informe de revisión, sin permisos para modificar tokens:
Lee figma-tokens-export.json y tokens.json.
Crea un informe Markdown con:
1. tokens presentes en Figma pero ausentes en el código
2. tokens presentes en el código pero ausentes en Figma
3. diferencias de valor entre tokens semánticos coincidentes
No edites tokens.json ni cambies nombres. Marca como arriesgadas las diferencias de foco, peligro y color de texto.
La meta no es sincronizar por sincronizar, sino obtener un diff pequeño, explicable y reversible que una persona pueda aprobar.
Casos de uso prácticos
Caso de uso 1: panel de administración SaaS
Botones, formularios, tablas y modales acumulan muchos estados. Claude Code puede inventariar el uso actual, proponer props compatibles y migrar una pantalla cada vez. La persona responsable del producto decide qué estados son obligatorios y acepta los cambios visuales.
Caso de uso 2: producto de marca blanca
Los colores primitivos cambian por cliente, mientras que tokens semánticos como surface, text o danger permanecen. Claude Code puede generar variables CSS por marca y selectores de tema en Storybook. El equipo de diseño aprueba contrastes y asociaciones de marca antes de publicar.
Caso de uso 3: limpieza de CSS heredado
Claude Code puede encontrar valores brutos repetidos, agruparlos como candidatos y crear una tabla de migración. No conviene sustituirlo todo en un único commit. Migra un componente, ejecuta snapshots y compara pantallas antes de continuar con el siguiente lote.
Caso de uso 4: embudo de marketing o contacto
Botones CTA, tarjetas de precios y estados de formulario consistentes reducen dudas del visitante y facilitan experimentos de conversión. El agente puede aplicar tokens y crear stories; una persona decide el mensaje, la jerarquía y si el cambio realmente mejora el recorrido.
Errores frecuentes: causa y corrección
Usar tokens primitivos directamente
Causa: los componentes dependen de nombres como blue-600, que describen apariencia y no intención. Corrección: introduce tokens semánticos o de componente, migra por lotes y comprueba visualmente cada cambio.
Mantener Storybook fuera de CI
Causa: la galería funciona en el portátil de una persona, pero puede romperse sin bloquear un pull request. Corrección: ejecuta vitest --project=storybook, compila Storybook y reserva el servidor estático para las capturas de Playwright.
Crear demasiados snapshots demasiado pronto
Causa: animaciones, fechas, fuentes externas e IDs aleatorios generan diferencias sin valor. Corrección: estabiliza los datos y empieza por estados críticos como normal, carga, error, desactivación y peligro.
Confundir un pase automático con accesibilidad completa
Causa: axe no comprende todo el contexto, la claridad del texto ni la calidad del recorrido de teclado. Corrección: añade una revisión humana con teclado y lector de pantalla antes de aceptar el componente.
Pedir una migración completa en una sola tarea
Causa: el diff mezcla decisiones de API, estilos y regresiones en demasiadas pantallas. Corrección: limita archivos y componente, exige pruebas, revisa el diff y solo entonces abre el lote siguiente.
Lista de comprobación antes de fusionar
La persona revisora debe confirmar:
- Los nombres de token expresan intención, no solo apariencia.
- Las props del componente son mínimas y estables.
- Storybook incluye estados desactivado, carga, error, foco y hover.
- El flujo completo funciona solo con teclado.
- ARIA se usa cuando hace falta y no sustituye HTML nativo correcto.
- Una persona revisó cada cambio de snapshot.
- El informe de diferencias con Figma queda guardado como evidencia.
- Claude Code solo modificó las áreas solicitadas.
- Prompts, logs, stories y capturas no contienen secretos ni datos privados.
Guarda esta lista en las instrucciones del proyecto. Así, la siguiente sesión no depende de recordar verbalmente las mismas restricciones.
Elige un único siguiente paso
Empieza por ejecutar el generador de tokens.json y revisar el CSS resultante. Después confirma que las stories de Button muestran todos los estados y que CI reproduce las pruebas y snapshots. Mantén Figma en modo de informe hasta que el equipo acuerde la fuente de verdad. Para implantar este flujo con acompañamiento, utiliza la página de formación y consulta sobre Claude Code.
Lo que se probó realmente
El 22 de julio de 2026 se extrajo el código build-tokens.mjs de este artículo y se ejecutó contra el ejemplo completo de tokens.json. Terminó con código 0, generó 25 propiedades personalizadas CSS y el mapa de tokens TypeScript, y resolvió {primitive.color.blue.600} como #2563eb. Una fixture negativa con una referencia desconocida terminó con código distinto de cero y el mensaje Unknown token reference. También se analizaron los fragmentos JSON y se comprobaron los bloques de código y enlaces internos. Los comandos de Storybook se contrastaron con la documentación oficial vigente del addon Vitest y su migración. Storybook no está instalado en el repositorio de este sitio; por eso, antes de adoptar el flujo, hay que ejecutar las pruebas de componentes y visuales en la aplicación de destino.
Artículos relacionados
Design Tokens con Claude Code: de Figma a variables CSS, Tailwind y React
Implementa design tokens con Claude Code, Style Dictionary, variables CSS, Tailwind y React.
CSS de produccion con Claude Code: capas, tokens y regresion visual
Usa Claude Code para ordenar CSS de produccion con capas, tokens, container queries, modo oscuro y checks visuales.
Variables CSS con Claude Code: tokens de tema practicos
Implementa variables CSS, var(), tokens de tema, modo oscuro y revisiones de UI con Claude Code.
PDF gratis: cheatsheet de Claude Code
Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.
Cuidamos tus datos y no enviamos spam.
Sobre el autor
Masa
Ingeniero enfocado en workflows prácticos con Claude Code.