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.
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.
| Caso | Estados en Storybook | Trabajo para Claude Code |
|---|---|---|
| Button de sistema de diseño | primary, secondary, loading, disabled, todos los tamaños | Tipos, stories, controls, autodocs |
| Formulario de registro gratuito | vacío, email inválido, envío, éxito | play, mocks, validación |
| CTA de artículo o producto | normal, destacado, móvil, texto largo | Responsive, 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'), '[email protected]');
await userEvent.click(canvas.getByRole('button', { name: 'Get the guide' }));
await expect(args.onSubmit).toHaveBeenCalledWith({
name: 'Masa',
email: '[email protected]',
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.
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.
Artículos relacionados
Checklist de permisos antes de que Claude Code edite un sitio de cliente
Guía para agencias que quieren usar IA en landing pages sin tocar zonas sensibles.
Convierte tickets de soporte SaaS en pasos reproducibles con Claude Code
Flujo para transformar reportes vagos en pasos, evidencia y una nota útil para ingeniería.
Convierte tus notas viejas de Obsidian en instrucciones para Claude Code en 10 minutos
Rutina de 10 minutos para separar tus notas de Obsidian en hechos, decisiones y dudas, y darle a Claude Code instrucciones que sí funcionan.