Use Cases (Actualizado: 2/6/2026)

Desarrollo con Storybook y Claude Code: catálogo UI, tests y CI

Configura Storybook con Claude Code: stories tipadas, interaction tests, CI y revisión visual con Chromatic.

Desarrollo con Storybook y Claude Code: catálogo UI, tests y CI

Storybook no es una galería: es un contrato de UI

Storybook permite desarrollar componentes fuera de la aplicación principal. Para alguien que empieza, es un catálogo donde se abre un botón, un formulario o una tarjeta de forma aislada. En un proyecto real, su valor está en dejar por escrito el contrato de la interfaz: props, estados, interacciones, accesibilidad, revisión de diseño y comprobaciones de CI.

Si le pides a Claude Code solo “añade Storybook”, puede generar unos cuantos Button.stories.tsx y terminar. Para que sea útil, conviene especificar qué componentes entran, qué estados son obligatorios, qué debe probar la función play y cómo se revisan los cambios visuales en CI con Chromatic o un flujo equivalente.

La guía usa React + TypeScript + Vite y patrones actuales de Storybook 8/9+: .storybook/main.ts, CSF con Meta/StoryObj, satisfies, interaction tests con @storybook/test, @storybook/addon-vitest y un workflow preparado para Chromatic. Las referencias oficiales consultadas son main configuration, TypeScript stories, interaction testing, Vitest addon y Chromatic for Storybook.

Si estás construyendo un sistema de diseño, también encaja con desarrollo de design systems con Claude Code y accesibilidad con Claude Code. Storybook funciona mejor cuando protege decisiones de diseño y flujos de usuario, no solo capturas bonitas.

Tres casos útiles para empezar

No hace falta meter toda la UI el primer día. Empieza por componentes que cambian con frecuencia y que afectan a registro, compra, consulta o lectura.

CasoEstados en StorybookTrabajo para Claude Code
Button de sistema de diseñoprimary, secondary, loading, disabled, todos los tamañosTipos, stories, controls, autodocs
Formulario de registro gratuitovacío, email inválido, envío, éxitoplay, mocks, validación
CTA de artículo o productonormal, destacado, móvil, texto largoResponsive, diff visual, enlaces internos

Con solo estos tres casos, diseño puede revisar estados, ingeniería valida el contrato de props y negocio comprueba que las CTAs importantes siguen funcionando.

Configuración basada en Vite

Para usar el Vitest addon, Storybook funciona mejor con un framework basado en Vite. En React + Vite usa @storybook/react-vite; en Next.js revisa @storybook/nextjs-vite.

npm create vite@latest storybook-claude-demo -- --template react-ts
cd storybook-claude-demo
npm install
npm create storybook@latest
npx storybook add @storybook/addon-a11y
npx storybook add @storybook/addon-vitest
npm install -D chromatic @vitest/browser-playwright playwright
npx playwright install chromium
{
  "scripts": {
    "dev": "vite",
    "storybook": "storybook dev -p 6006",
    "build-storybook": "storybook build",
    "test-storybook": "vitest --project=storybook --run",
    "chromatic": "chromatic"
  }
}
// .storybook/main.ts
import type { StorybookConfig } from '@storybook/react-vite';

const config: StorybookConfig = {
  framework: '@storybook/react-vite',
  stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
  addons: ['@storybook/addon-docs', '@storybook/addon-a11y', '@storybook/addon-vitest'],
  docs: {
    defaultName: 'Docs',
  },
  staticDirs: ['../public'],
};

export default config;
// .storybook/preview.ts
import type { Preview } from '@storybook/react-vite';

const preview: Preview = {
  tags: ['autodocs'],
  parameters: {
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/i,
      },
    },
    a11y: {
      test: 'todo',
    },
  },
};

export default preview;

Ejemplo 1: Button tipado con stories reales

Evita props inventadas solo para Storybook. La story debe reflejar el componente real.

// src/components/Button.tsx
import type { ButtonHTMLAttributes, CSSProperties, ReactNode } from 'react';

type ButtonVariant = 'primary' | 'secondary' | 'outline';
type ButtonSize = 'sm' | 'md' | 'lg';

export type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
  children: ReactNode;
  variant?: ButtonVariant;
  size?: ButtonSize;
  loading?: boolean;
};

const variantStyle: Record<ButtonVariant, CSSProperties> = {
  primary: { background: '#2563eb', color: '#ffffff', borderColor: '#2563eb' },
  secondary: { background: '#0f172a', color: '#ffffff', borderColor: '#0f172a' },
  outline: { background: '#ffffff', color: '#0f172a', borderColor: '#94a3b8' },
};

const sizeStyle: Record<ButtonSize, CSSProperties> = {
  sm: { minHeight: 32, padding: '0 12px', fontSize: 14 },
  md: { minHeight: 40, padding: '0 16px', fontSize: 15 },
  lg: { minHeight: 48, padding: '0 20px', fontSize: 16 },
};

export function Button({
  children,
  variant = 'primary',
  size = 'md',
  loading = false,
  disabled,
  style,
  ...props
}: ButtonProps) {
  return (
    <button
      {...props}
      disabled={disabled || loading}
      aria-busy={loading || undefined}
      style={{
        ...variantStyle[variant],
        ...sizeStyle[size],
        borderWidth: 1,
        borderStyle: 'solid',
        borderRadius: 6,
        fontWeight: 700,
        cursor: disabled || loading ? 'not-allowed' : 'pointer',
        opacity: disabled ? 0.55 : 1,
        ...style,
      }}
    >
      {loading ? 'Sending...' : children}
    </button>
  );
}
// src/components/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react-vite';
import { Button } from './Button';

const meta = {
  title: 'Components/Button',
  component: Button,
  tags: ['autodocs'],
  args: {
    children: 'Start free trial',
    variant: 'primary',
    size: 'md',
  },
  argTypes: {
    variant: {
      control: 'select',
      options: ['primary', 'secondary', 'outline'],
    },
    size: {
      control: 'inline-radio',
      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 = {} satisfies Story;

export const Secondary = {
  args: {
    variant: 'secondary',
    children: 'View pricing',
  },
} satisfies Story;

export const AllSizes = {
  render: () => (
    <div style={{ display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap' }}>
      <Button size="sm">Small</Button>
      <Button size="md">Medium</Button>
      <Button size="lg">Large</Button>
    </div>
  ),
} satisfies Story;

export const Loading = {
  args: {
    loading: true,
  },
} satisfies Story;

Ejemplo 2: interaction test para un formulario

La función play reproduce acciones después del render. Así se comprueba si el usuario puede escribir, enviar y ver errores.

// src/components/SignupForm.tsx
import type { FormEvent } from 'react';
import { useState } from 'react';
import { Button } from './Button';

export type SignupFormData = {
  name: string;
  email: string;
  plan: 'free' | 'team';
};

type SignupFormProps = {
  plan?: SignupFormData['plan'];
  onSubmit?: (data: SignupFormData) => Promise<void> | void;
};

export function SignupForm({ plan = 'free', onSubmit }: SignupFormProps) {
  const [message, setMessage] = useState('');

  async function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const form = new FormData(event.currentTarget);
    const data: SignupFormData = {
      name: String(form.get('name') ?? ''),
      email: String(form.get('email') ?? ''),
      plan,
    };

    if (!data.email.includes('@')) {
      setMessage('Please enter a valid email address.');
      return;
    }

    await onSubmit?.(data);
    setMessage('Thanks, we will send the setup guide.');
  }

  return (
    <form onSubmit={handleSubmit} style={{ display: 'grid', gap: 12, maxWidth: 360 }}>
      <label>
        Name
        <input name="name" required style={{ display: 'block', width: '100%', minHeight: 36 }} />
      </label>
      <label>
        Email
        <input name="email" type="email" required style={{ display: 'block', width: '100%', minHeight: 36 }} />
      </label>
      <Button type="submit">Get the guide</Button>
      {message ? <p role="status">{message}</p> : null}
    </form>
  );
}
// src/components/SignupForm.stories.tsx
import type { Meta, StoryObj } from '@storybook/react-vite';
import { expect, fn, userEvent, within } from '@storybook/test';
import { SignupForm } from './SignupForm';

const meta = {
  title: 'Components/SignupForm',
  component: SignupForm,
  args: {
    plan: 'team',
    onSubmit: fn(),
  },
} satisfies Meta<typeof SignupForm>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Empty = {} satisfies Story;

export const InvalidEmail = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);

    await userEvent.type(canvas.getByLabelText('Name'), 'Masa');
    await userEvent.type(canvas.getByLabelText('Email'), 'masa.example.com');
    await userEvent.click(canvas.getByRole('button', { name: 'Get the guide' }));

    await expect(canvas.getByRole('status')).toHaveTextContent('valid email');
  },
} satisfies Story;

export const SuccessfulSubmit = {
  play: async ({ canvasElement, args }) => {
    const canvas = within(canvasElement);

    await userEvent.type(canvas.getByLabelText('Name'), 'Masa');
    await userEvent.type(canvas.getByLabelText('Email'), 'masa@example.com');
    await userEvent.click(canvas.getByRole('button', { name: 'Get the guide' }));

    await expect(args.onSubmit).toHaveBeenCalledWith({
      name: 'Masa',
      email: 'masa@example.com',
      plan: 'team',
    });
    await expect(canvas.getByRole('status')).toHaveTextContent('setup guide');
  },
} satisfies Story;

Ejemplo 3: Vitest y CI con revisión visual

El Vitest addon convierte las stories en tests de componentes en navegador. Si hay play, ejecuta interacciones y aserciones.

// .storybook/vitest.setup.ts
import { setProjectAnnotations } from '@storybook/react-vite';
import * as previewAnnotations from './preview';

setProjectAnnotations([previewAnnotations]);
// vitest.config.ts
import { defineConfig, defineProject, mergeConfig } from 'vitest/config';
import { playwright } from '@vitest/browser-playwright';
import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import viteConfig from './vite.config';

const dirname = path.dirname(fileURLToPath(import.meta.url));

export default mergeConfig(
  viteConfig,
  defineConfig({
    test: {
      projects: [
        defineProject({
          extends: true,
          plugins: [
            storybookTest({
              configDir: path.join(dirname, '.storybook'),
              storybookScript: 'npm run storybook -- --ci',
              storybookUrl: 'http://localhost:6006',
              tags: {
                include: ['test'],
                exclude: ['experimental'],
              },
            }),
          ],
          test: {
            name: 'storybook',
            browser: {
              enabled: true,
              provider: playwright({}),
              headless: true,
              instances: [{ browser: 'chromium' }],
            },
            setupFiles: ['./.storybook/vitest.setup.ts'],
          },
        }),
      ],
    },
  }),
);
# .github/workflows/storybook.yml
name: storybook

on:
  pull_request:

jobs:
  verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npx playwright install --with-deps chromium
      - run: npm run test-storybook
      - run: npm run build-storybook
      - name: Publish to Chromatic
        if: ${{ secrets.CHROMATIC_PROJECT_TOKEN != '' }}
        run: npx chromatic --project-token=${{ secrets.CHROMATIC_PROJECT_TOKEN }}

Plantilla para pedirlo a Claude Code

Configura Storybook en un proyecto React + TypeScript + Vite.

Alcance:
- src/components/Button.tsx
- src/components/SignupForm.tsx

Requisitos:
- Usar @storybook/react-vite en .storybook/main.ts
- Escribir CSF con Meta/StoryObj y satisfies
- Crear stories de Button para primary, secondary, outline, loading, disabled y todos los tamaños
- Crear play functions de SignupForm para invalid email y successful submit
- Usar within, userEvent, expect y fn desde @storybook/test
- Configurar Vitest addon para ejecutar test-storybook en CI
- Añadir build-storybook y una revisión visual tipo Chromatic en GitHub Actions

No hagas:
- No uses pseudocódigo
- No agregues props que no existan en el componente real
- No escribas secrets en el código
- No formatees archivos fuera del alcance

Al terminar, informa:
- Archivos modificados
- Comandos ejecutados
- Riesgos restantes

Errores frecuentes

El primer error es crear props solo para Storybook. El segundo es probar solo el camino feliz y olvidar email inválido, loading, disabled o doble envío. El tercero es dejar fechas, valores aleatorios, animaciones e imágenes remotas sin estabilizar, lo que llena Chromatic de ruido. El cuarto es mezclar @storybook/react, @storybook/react-vite y @storybook/nextjs-vite. El quinto es olvidar instalar Chromium en CI. El sexto es dejar para el final las CTAs, tarjetas de producto y formularios de consulta, que son justamente las piezas más cercanas a ingresos.

CTA y resultado práctico

En un medio técnico o sitio de producto, Storybook también protege la monetización. Formularios de descarga gratuita, CTAs de productos, formularios de consultoría y tarjetas de enlaces internos pueden convertirse en stories y revisarse en cada PR.

Para empezar con prompts reutilizables, usa la chuleta gratuita. Para convertirlo en plantillas, revisa productos y plantillas. Si necesitas Storybook, CI, Chromatic, accesibilidad y revisión de CTAs en un repositorio real, entra por formación y consultoría de Claude Code.

Al probarlo en un proyecto React pequeño, lo que más redujo retrabajo no fue la variedad de Button, sino la story fallida del formulario. Separar InvalidEmail y SuccessfulSubmit, y ejecutar test-storybook junto con build-storybook, hizo que los problemas de CTA y formulario aparecieran antes de pedir más cambios a Claude Code.

#Claude Code #Storybook #desarrollo UI #components #frontend
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.