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'), '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.
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
Workflow de Obsidian a CLAUDE.md con Claude Code
Convierte notas de trabajo de Obsidian en notas operativas de CLAUDE.md para no repetir contexto.
Claude Code Revenue CTA Routing: de artículos a PDF, Gumroad y consulta
Un flujo con Claude Code para dirigir lectores a PDF gratis, Gumroad o consulta según intención.
Reglas de handoff para equipos con Claude Code: evidencia, permisos, rollback e ingresos
Formato práctico para entregar trabajo de Claude Code con pruebas, permisos, rollback, PDF gratis, Gumroad y consulta.