Desenvolvimento com Storybook e Claude Code: UI, testes e CI
Configure Storybook com Claude Code: stories tipadas, interaction tests, CI e revisão visual com Chromatic.
Storybook não é uma vitrine: é um contrato de UI
Storybook permite desenvolver componentes fora da aplicação principal. Para quem está começando, ele funciona como um catálogo onde você abre um botão, formulário ou card de forma isolada. Em projetos reais, o valor é maior: props, estados, interações, acessibilidade, revisão de design e verificações de CI ficam documentados no mesmo lugar.
Se você pedir apenas “adicione Storybook” ao Claude Code, ele pode criar alguns arquivos Button.stories.tsx e parar. Um setup útil precisa dizer quais componentes entram, quais estados são obrigatórios, o que a função play deve testar e como o PR executa Chromatic ou uma validação visual equivalente.
Este guia usa React + TypeScript + Vite com práticas atuais para Storybook 8/9+: .storybook/main.ts, CSF com Meta/StoryObj, satisfies, interaction tests com @storybook/test, @storybook/addon-vitest e CI pronto para Chromatic. As referências oficiais usadas foram main configuration, TypeScript stories, interaction testing, Vitest addon e Chromatic for Storybook.
Se você também está criando um design system, leia junto design system com Claude Code e acessibilidade com Claude Code. Storybook funciona melhor quando protege decisões e fluxos do usuário, não só screenshots.
Três casos para começar
Não leve toda a interface para o Storybook de uma vez. Comece por componentes que mudam com frequência e ficam perto de cadastro, compra ou contato.
| Caso | Estados no Storybook | Trabalho para Claude Code |
|---|---|---|
| Button do design system | primary, secondary, loading, disabled, todos os tamanhos | Tipos, stories, controls, autodocs |
| Formulário gratuito | vazio, email inválido, envio, sucesso | play, mocks, validação |
| CTA de artigo ou produto | padrão, destaque, mobile, texto longo | Responsivo, diff visual, links internos |
Esses três casos já ajudam design, engenharia e negócio a revisar a mesma UI por ângulos diferentes.
Configuração baseada em Vite
Para o Vitest addon, o caminho mais simples é usar um framework Storybook baseado em Vite. Em React + Vite use @storybook/react-vite; em Next.js avalie @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;
Exemplo 1: Button com stories tipadas
Não crie props só para o Storybook. A story deve refletir o contrato real do componente.
// 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;
Exemplo 2: interaction test para formulário
A função play roda depois que a story renderiza. Ela permite validar digitação, erro e envio.
// 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;
Exemplo 3: Vitest e CI com revisão visual
O Vitest addon transforma stories em testes de componentes no navegador. Stories com play executam interação e assertions.
// .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 }}
Template de pedido para Claude Code
Configure Storybook em um projeto React + TypeScript + Vite.
Escopo:
- src/components/Button.tsx
- src/components/SignupForm.tsx
Requisitos:
- Usar @storybook/react-vite em .storybook/main.ts
- Escrever CSF com Meta/StoryObj e satisfies
- Criar stories de Button para primary, secondary, outline, loading, disabled e todos os tamanhos
- Criar play functions de SignupForm para invalid email e successful submit
- Usar within, userEvent, expect e fn de @storybook/test
- Configurar Vitest addon para rodar test-storybook em CI
- Adicionar build-storybook e uma verificação visual estilo Chromatic no GitHub Actions
Não faça:
- Não use pseudocódigo
- Não adicione props que não existem no componente real
- Não escreva secrets no código
- Não formate arquivos fora do escopo
Ao terminar, informe:
- Arquivos alterados
- Comandos executados
- Riscos restantes
Armadilhas comuns
A primeira é criar props só para Storybook. A segunda é testar apenas o caminho feliz e esquecer email inválido, loading, disabled e envio duplicado. A terceira é deixar datas, valores aleatórios, animações e imagens externas gerarem diffs visuais inúteis. A quarta é misturar @storybook/react, @storybook/react-vite e @storybook/nextjs-vite. A quinta é esquecer Chromium no CI. A sexta é deixar CTAs, cards de produto e formulários de contato para depois, mesmo sendo a parte mais próxima da receita.
CTA e resultado prático
Em mídia técnica ou site de produto, Storybook também protege monetização. Formulários de download gratuito, CTAs de produto, formulários de consultoria e cards de links internos podem virar stories revisadas em cada PR.
Para começar com prompts reutilizáveis, use o cheat sheet gratuito. Para transformar o fluxo em templates, veja produtos e modelos. Para integrar Storybook, CI, Chromatic, acessibilidade e revisão de CTA em um repositório real, use treinamento e consultoria Claude Code.
No teste com um projeto React pequeno, o maior ganho não veio das variações do Button, mas da story de erro do SignupForm. Separar InvalidEmail e SuccessfulSubmit e rodar test-storybook com build-storybook em CI reduziu bastante as correções pedidas ao Claude Code.
PDF grátis: cheatsheet do Claude Code
Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.
Cuidamos dos seus dados e não enviamos spam.
Sobre o autor
Masa
Engenheiro focado em workflows práticos com Claude Code.
Artigos relacionados
Workflow Obsidian para CLAUDE.md com Claude Code
Transforme notas de trabalho do Obsidian em notas operacionais CLAUDE.md para preservar contexto.
Claude Code Revenue CTA Routing: artigos para PDF, Gumroad e consultoria
Um fluxo com Claude Code para levar leitores ao PDF grátis, Gumroad ou consultoria conforme intenção.
Regras de handoff para equipes com Claude Code: evidências, permissões, rollback e receita
Formato prático para entregar trabalho do Claude Code com prova, permissões, rollback, PDF grátis, Gumroad e consultoria.