Développer avec Storybook et Claude Code : catalogue UI, tests et CI
Configurer Storybook avec Claude Code : stories typées, interaction tests, CI et revue visuelle Chromatic.
Storybook n’est pas une galerie, c’est un contrat d’interface
Storybook permet de développer des composants UI en dehors de l’application principale. Pour débuter, on peut le voir comme un catalogue où l’on ouvre un bouton, un formulaire ou une carte de façon isolée. En production, son intérêt est plus fort : il documente les props, les états, les interactions, l’accessibilité, les revues de design et les contrôles CI.
Si vous demandez simplement à Claude Code “ajoute Storybook”, il risque de produire quelques fichiers Button.stories.tsx et de s’arrêter. Pour obtenir un outil maintenable, il faut préciser les composants prioritaires, les états obligatoires, les interactions à vérifier avec play, et la façon d’exécuter Chromatic ou un contrôle visuel équivalent dans les pull requests.
Ce guide utilise React + TypeScript + Vite avec des pratiques compatibles Storybook 8/9+ : .storybook/main.ts, CSF avec Meta/StoryObj, satisfies, les interaction tests de @storybook/test, @storybook/addon-vitest, et un workflow CI prêt pour Chromatic. Les références officielles consultées sont main configuration, TypeScript stories, interaction testing, Vitest addon et Chromatic for Storybook.
Pour aller plus loin, reliez ce travail à Claude Code pour les design systems et Claude Code pour l’accessibilité. Storybook devient vraiment utile quand il protège les décisions de design et les parcours utilisateur, pas seulement des captures d’écran.
Trois cas d’usage à traiter d’abord
Il n’est pas nécessaire d’ajouter toute l’interface en une fois. Commencez par les composants qui changent souvent et qui touchent l’inscription, la vente ou la demande de contact.
| Cas | États à couvrir | Travail à confier à Claude Code |
|---|---|---|
| Button du design system | primary, secondary, loading, disabled, toutes les tailles | Types, stories, controls, autodocs |
| Formulaire d’inscription gratuit | vide, email invalide, envoi, succès | play, mocks, validation |
| CTA d’article ou de produit | standard, accentué, mobile, texte long | Responsive, diff visuel, liens internes |
Avec ces trois exemples, le design vérifie les états, le développement valide le contrat de props, et l’équipe business surveille les éléments proches de la conversion.
Configuration Vite recommandée
Le Vitest addon de Storybook fonctionne le plus simplement avec un framework basé sur Vite. En React + Vite, utilisez @storybook/react-vite; en Next.js, évaluez @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;
Exemple 1 : Button avec stories typées
N’ajoutez pas de props réservées à Storybook. Une story doit refléter le composant réel.
// 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;
Exemple 2 : tester un formulaire avec play
La fonction play s’exécute après le rendu de la story. Elle permet de tester les saisies, les erreurs et l’envoi.
// 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;
Exemple 3 : Vitest et CI visuelle
Le Vitest addon transforme les stories en tests de composants exécutés dans un vrai navigateur. Les stories avec play exécutent aussi leurs interactions.
// .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 }}
Modèle de demande pour Claude Code
Configure Storybook pour un projet React + TypeScript + Vite.
Périmètre:
- src/components/Button.tsx
- src/components/SignupForm.tsx
Exigences:
- Utiliser @storybook/react-vite dans .storybook/main.ts
- Écrire les stories en CSF avec Meta/StoryObj et satisfies
- Couvrir Button: primary, secondary, outline, loading, disabled, all sizes
- Ajouter les play functions de SignupForm pour invalid email et successful submit
- Utiliser within, userEvent, expect et fn depuis @storybook/test
- Configurer Vitest addon pour lancer test-storybook en CI
- Ajouter build-storybook et une vérification visuelle type Chromatic dans GitHub Actions
À éviter:
- Pas de pseudocode
- Ne pas ajouter de props absentes du vrai composant
- Ne pas écrire de secrets dans le code
- Ne pas formater les fichiers hors périmètre
Rapport final:
- Fichiers modifiés
- Commandes exécutées
- Risques restants
Pièges fréquents
Le premier piège est d’ajouter des props uniquement pour Storybook. Le deuxième est de tester seulement le chemin nominal et d’oublier email invalide, loading, disabled ou double soumission. Le troisième est de laisser les dates, valeurs aléatoires, animations et images distantes produire des diffs visuels inutiles. Le quatrième est de mélanger @storybook/react, @storybook/react-vite et @storybook/nextjs-vite. Le cinquième est d’oublier Chromium en CI. Le sixième est de repousser les CTA, cartes produit et formulaires de contact, alors qu’ils sont proches du revenu.
CTA et retour d’expérience
Pour un média technique ou un site produit, Storybook protège aussi la monétisation. Les formulaires de téléchargement gratuit, CTA produit, formulaires de conseil et cartes de liens internes peuvent devenir des stories testées et revues à chaque PR.
Pour commencer avec des prompts réutilisables, prenez la fiche gratuite. Pour industrialiser le flux, consultez produits et modèles. Pour intégrer Storybook, CI, Chromatic, accessibilité et revue des CTA dans un vrai dépôt, passez par formation et conseil Claude Code.
Lors d’un essai sur un petit projet React, le plus utile n’a pas été la variante de couleur du Button, mais la story d’échec du formulaire. Séparer InvalidEmail et SuccessfulSubmit, puis exécuter test-storybook et build-storybook en CI, a réduit les corrections demandées à Claude Code.
PDF gratuit: cheatsheet Claude Code
Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.
Nous protégeons vos données et n'envoyons pas de spam.
À propos de l'auteur
Masa
Ingénieur spécialisé dans les workflows pratiques avec Claude Code.
Articles liés
Workflow Obsidian vers CLAUDE.md avec Claude Code
Transformer des notes Obsidian en notes CLAUDE.md concises pour reprendre les sessions sans réexpliquer.
Claude Code Revenue CTA Routing : relier articles, PDF, Gumroad et consultation
Un workflow Claude Code pour orienter les lecteurs vers PDF gratuit, Gumroad ou consultation selon l'intention.
Règles de handoff Claude Code en équipe: preuves, permissions, rollback et revenus
Un format concret pour transmettre un travail Claude Code avec preuves, permissions, rollback, PDF gratuit, Gumroad et consultation.