Storybook mit Claude Code entwickeln: UI-Katalog, Tests und CI
Storybook mit Claude Code einrichten: typisierte Stories, Interaction Tests, CI und Chromatic-Review.
Storybook ist kein Bilderalbum, sondern ein UI-Vertrag
Storybook entwickelt UI-Komponenten außerhalb der Hauptanwendung. Für Einsteiger ist es ein Katalog, in dem man Buttons, Formulare oder Karten einzeln öffnet. Im Projektalltag ist es wertvoller: Es dokumentiert Props, Zustände, Interaktionen, Accessibility, Design-Review und CI-Prüfungen an einer Stelle.
Wenn du Claude Code nur bittest, “Storybook hinzuzufügen”, entstehen oft ein paar Button.stories.tsx Dateien. Das reicht selten. Ein belastbares Setup legt vorher fest, welche Komponenten relevant sind, welche Zustände abgedeckt werden, was die play Funktion testet und wie Chromatic oder eine vergleichbare visuelle Prüfung im Pull Request läuft.
Dieser Leitfaden nutzt React + TypeScript + Vite und aktuelle Storybook-8/9+-Muster: .storybook/main.ts, CSF mit Meta/StoryObj, satisfies, Interaction Tests mit @storybook/test, @storybook/addon-vitest und CI mit Chromatic-Anbindung. Geprüft wurden die offiziellen Seiten zu main configuration, TypeScript stories, interaction testing, Vitest addon und Chromatic for Storybook.
Wenn du parallel ein Design System aufbaust, passen Claude Code Design System und Claude Code Accessibility dazu. Storybook ist am stärksten, wenn es Entscheidungen und Nutzerflüsse schützt.
Drei sinnvolle Startpunkte
Nicht jede UI muss sofort in Storybook. Beginne mit Komponenten, die sich oft ändern und nahe an Registrierung, Kauf oder Kontakt liegen.
| Beispiel | Zustände in Storybook | Aufgabe für Claude Code |
|---|---|---|
| Design-System Button | primary, secondary, loading, disabled, alle Größen | Props, Stories, Controls, Autodocs |
| Kostenloses Signup-Formular | leer, ungültige Eingabe, Absenden, Erfolg | play, Mocks, Validierung |
| Artikel- oder Produkt-CTA | Standard, hervorgehoben, mobil, langer Text | Responsive-Check, visueller Diff, interne Links |
Damit können Design, Entwicklung und Business dieselbe UI aus unterschiedlichen Blickwinkeln prüfen.
Vite als Basis verwenden
Für das Vitest addon ist ein Vite-basiertes Storybook-Framework am einfachsten. In React + Vite nutzt du @storybook/react-vite; in Next.js prüfst du @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;
Beispiel 1: Button mit typisierten Stories
Erfinde keine Props nur für Storybook. Die Story muss den echten Vertrag der Komponente zeigen.
// 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;
Beispiel 2: Formular per Interaction Test prüfen
Die play Funktion läuft nach dem Rendern der Story. So testest du Eingabe, Fehler und erfolgreichen Submit.
// 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;
Beispiel 3: Vitest und Chromatic-ähnliche CI
Das Vitest addon macht aus Stories Browser-Komponententests. Stories mit play führen auch Interaktionen und Assertions aus.
// .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 }}
Prompt-Vorlage für Claude Code
Richte Storybook für ein React + TypeScript + Vite Projekt ein.
Scope:
- src/components/Button.tsx
- src/components/SignupForm.tsx
Anforderungen:
- .storybook/main.ts nutzt @storybook/react-vite
- CSF mit Meta/StoryObj und satisfies schreiben
- Button Stories für primary, secondary, outline, loading, disabled und alle Größen
- SignupForm play functions für invalid email und successful submit
- within, userEvent, expect und fn aus @storybook/test verwenden
- Vitest addon konfigurieren, damit test-storybook in CI läuft
- build-storybook und visuellen Check ähnlich Chromatic in GitHub Actions hinzufügen
Nicht tun:
- Kein Pseudocode
- Keine Props hinzufügen, die die echte Komponente nicht hat
- Keine Secrets in Code schreiben
- Keine Dateien außerhalb des Scopes formatieren
Berichte am Ende:
- Geänderte Dateien
- Ausgeführte Befehle
- Restliche Risiken
Häufige Fehler
Der erste Fehler sind Props nur für Storybook. Der zweite ist nur der Happy Path: ungültige Eingaben, loading, disabled und doppelte Submits fehlen dann. Der dritte sind instabile visuelle Diffs durch Datum, Zufall, Animationen oder externe Bilder. Der vierte ist ein Mix aus @storybook/react, @storybook/react-vite und @storybook/nextjs-vite. Der fünfte ist ein CI ohne installierten Chromium. Der sechste ist, Umsatz-nahe CTAs, Produktkarten und Kontaktformulare erst spät zu testen.
CTA und Praxisergebnis
Für technische Medien und Produktseiten schützt Storybook auch den Umsatzpfad. Freebie-Formulare, Produkt-CTAs, Beratungsformulare und interne Link-Karten können als Stories getestet und visuell geprüft werden.
Für wiederverwendbare Claude-Code-Prompts starte mit dem kostenlosen Cheat Sheet. Für Vorlagen nutze Produkte und Templates. Für ein echtes Repository mit Storybook, CI, Chromatic, Accessibility und CTA-Review ist Claude Code Training und Beratung der passende Einstieg.
Im kleinen React-Testprojekt brachte nicht die Button-Farbvariante den größten Nutzen, sondern die fehlschlagende SignupForm-Story. InvalidEmail und SuccessfulSubmit getrennt zu testen und in CI test-storybook sowie build-storybook laufen zu lassen, reduzierte die Nacharbeit mit Claude Code deutlich.
Kostenloses PDF: Claude-Code-Cheatsheet
E-Mail eintragen und eine Seite mit Befehlen, Review-Gewohnheiten und sicheren Workflows herunterladen.
Wir schützen Ihre Daten und senden keinen Spam.
Über den Autor
Masa
Engineer für praktische Claude-Code-Workflows und Team-Einführung.
Ähnliche Artikel
Claude Code Workflow von Obsidian zu CLAUDE.md
Obsidian-Arbeitsnotizen in CLAUDE.md-Betriebsnotizen verwandeln und Kontext nicht ständig neu erklären.
Claude Code Revenue CTA Routing: Artikel zu PDF, Gumroad und Beratung führen
Ein Claude-Code-Ablauf, der Leser nach Absicht zu Gratis-PDF, Gumroad oder Beratung führt.
Claude-Code-Team-Handoff-Regeln: Belege, Berechtigungen, Rollback und Umsatzpfade
Ein praktisches Claude-Code-Handoff für Review-Belege, Berechtigungen, Rollback, Gratis-PDF, Gumroad und Beratung.