Advanced (Actualizado: 1/6/2026)

Construir un sistema de diseño con Claude Code: Design Tokens, Storybook y CI

Guía práctica para usar Claude Code con Design Tokens, React/TypeScript, Storybook, accesibilidad, visual tests y CI.

Construir un sistema de diseño con Claude Code: Design Tokens, Storybook y CI

Un sistema de diseño es una operación, no una galería de componentes

Crear botones y tarjetas es solo el comienzo. Un sistema de diseño útil define cómo cambian los colores, el espaciado, la tipografía, los estados, las revisiones y las pruebas sin romper las pantallas del producto.

Claude Code encaja bien en este trabajo porque puede leer el repositorio, editar varios archivos, ejecutar Storybook y pruebas, y devolver un resumen de los cambios. Pero no debe decidir por sí solo la marca, la intención de Figma ni la aprobación final de accesibilidad.

En esta guía veremos Design Tokens, componentes React/TypeScript, Storybook, accesibilidad, pruebas visuales y a11y en CI, límites realistas con Figma y el nivel de detalle que conviene dar a Claude Code. También puedes revisar gestión de Design Tokens, desarrollo con Storybook y accesibilidad con Claude Code.

Arquitectura objetivo

El contrato del lado del código serátokens.json. Figma sigue siendo una fuente clave de diseño, pero los cambios que llegan al producto deben ser revisables y comprobables en CI.

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 con nombre: colores, espacios, radios, tipografía y estados. Un componente debería usar un nombre semántico comoaction.background.primary en lugar de un valor crudo como#2563eb.

Para mantenerte actualizado, consulta Design Tokens Community Group, documentación de Claude Code, seguridad de Claude Code, Storybook accessibility testing, Storybook visual tests, Playwright accessibility testing y Figma REST API.

Qué pedirle a Claude Code

Una petición como “haz un sistema de diseño” es demasiado amplia. Funciona mejor algo como: “migra soloButton, conserva la API pública, añade estados en Storybook y ejecuta pruebas a11y y visuales”.

ÁreaBuen alcance para Claude CodeDecisión humana
TokensExtraer colores y espacios repetidosSignificado de marca y nombres
ComponentesImplementarButton, Input y Alert tipadosAPI pública y semántica del producto
StorybookCrear variants, states e historias de interacciónEstados necesarios en flujos reales
AccesibilidadDetectar labels, focus y violaciones axeRevisión final de UX y lector de pantalla
CIConectar pruebas visuales y a11yPolítica de bloqueo y excepciones

Usa reglas explícitas antes de editar:

Design system task rules:
- Edit only src/components, src/styles, .storybook, tests, scripts, and tokens.json.
- Do not change brand colors without listing old and new token names.
- Every new component needs TypeScript props, keyboard behavior, Storybook stories, and a11y notes.
- Run npm run tokens:build, npm run test:storybook, npm run test:a11y, and npm run test:visual before reporting done.
- If focus behavior changes, include manual review steps.

La seguridad importa. No pegues tokens de Figma, npm tokens, secretos de CI ni capturas de clientes en prompts. Revisa los comandos antes de aprobarlos y trata las actualizaciones masivas de snapshots como cambios que requieren revisión humana.

Instalación mínima

Ejemplo para React y TypeScript:

npm install class-variance-authority clsx tailwind-merge
npm install -D @storybook/react-vite @storybook/addon-a11y @storybook/test-runner @playwright/test @axe-core/playwright concurrently http-server wait-on
npx storybook init
npx playwright install chromium

Scripts recomendados:

{
  "scripts": {
    "tokens:build": "node scripts/build-tokens.mjs",
    "storybook": "storybook dev -p 6006",
    "build-storybook": "storybook build",
    "test:storybook": "test-storybook --url http://127.0.0.1:6006",
    "test:a11y": "playwright test tests/a11y.spec.ts",
    "test:visual": "playwright test tests/button.visual.spec.ts"
  }
}

Convertir los tokens en contrato

Divide los tokens en primitive, semantic y component.

{
  "primitive": {
    "color": {
      "blue": {
        "50": { "$type": "color", "$value": "#eff6ff" },
        "600": { "$type": "color", "$value": "#2563eb" },
        "700": { "$type": "color", "$value": "#1d4ed8" }
      },
      "gray": {
        "50": { "$type": "color", "$value": "#f9fafb" },
        "200": { "$type": "color", "$value": "#e5e7eb" },
        "900": { "$type": "color", "$value": "#111827" }
      },
      "red": {
        "600": { "$type": "color", "$value": "#dc2626" },
        "700": { "$type": "color", "$value": "#b91c1c" }
      },
      "white": { "$type": "color", "$value": "#ffffff" }
    },
    "space": {
      "2": { "$type": "dimension", "$value": "0.5rem" },
      "3": { "$type": "dimension", "$value": "0.75rem" },
      "4": { "$type": "dimension", "$value": "1rem" },
      "6": { "$type": "dimension", "$value": "1.5rem" }
    },
    "radius": {
      "md": { "$type": "dimension", "$value": "0.375rem" },
      "lg": { "$type": "dimension", "$value": "0.5rem" }
    }
  },
  "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 CSS variables y TypeScript:

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) {
  if (typeof value === "string" && value.startsWith("{") && value.endsWith("}")) {
    return resolveValue(getToken(value.slice(1, -1)));
  }
  return value;
}

function walk(node, pathParts = [], result = {}) {
  if (node && typeof node === "object" && typeof node.$value !== "undefined") {
    result[pathParts.join("-")] = resolveValue(node.$value);
    return result;
  }
  for (const [key, value] of Object.entries(node)) {
    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.`);

Componente React/TypeScript

El componente debe ser predecible y accesible.

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>
  );
});

Storybook y CI

Cada estado importante debe tener una story.

import type { Meta, StoryObj } from "@storybook/react";
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: "Guardar", variant: "primary" } };
export const Danger: Story = { args: { children: "Eliminar", variant: "danger" } };
export const Loading: Story = { args: { children: "Guardando", 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>
  )
};

Prueba a11y con Playwright:

import { expect, test } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright";

const storyPaths = [
  "/iframe.html?id=design-system-button--primary",
  "/iframe.html?id=design-system-button--danger",
  "/iframe.html?id=design-system-button--loading",
  "/iframe.html?id=design-system-button--all-states"
];

for (const storyPath of storyPaths) {
  test(`a11y ${storyPath}`, async ({ page }) => {
    await page.goto(`http://127.0.0.1:6006${storyPath}`);
    const results = await new AxeBuilder({ page })
      .withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"])
      .analyze();
    expect(results.violations).toEqual([]);
  });
}

Prueba visual:

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"
  });
});

Workflow de CI:

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: npm run build-storybook
      - run: npx playwright install --with-deps chromium
      - 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:storybook && npm run test:a11y && npm run test:visual"

Figma, casos reales y errores

Con Figma, empieza con informes de diferencias, no con sincronización automática.

Read figma-tokens-export.json and tokens.json.
Create a markdown report with:
1. tokens that exist in Figma but not in code
2. tokens that exist in code but not in Figma
3. value differences for matching semantic tokens
Do not edit tokens.json. Do not rename tokens. Mark risky differences around focus, danger, and text color.

Tres casos claros: un panel SaaS con muchos estados de formularios, un producto white-label con colores por marca y un legado CSS lleno de valores repetidos. En los tres, Claude Code debe empezar con inventario y cambios pequeños.

Los fallos habituales son usar solo tokens primitivos, dejar Storybook fuera de CI, crear demasiados snapshots ruidosos, pensar que axe sustituye la revisión manual y pedir una migración enorme en una sola tarea.

Antes de aplicarlo, verifica quetokens.json genera CSS y TypeScript, que Storybook muestra todos los estados y que CI ejecuta build, a11y y visual test de forma estable. Para apoyo en implementación, formación o revisión de diseño de sistemas con Claude Code, puedes usar la página de training y consulta.

#Claude Code #sistema de diseño #Design Tokens #Storybook #accesibilidad
Gratis

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.

Masa

Sobre el autor

Masa

Ingeniero enfocado en workflows prácticos con Claude Code.