Claude CodeでStorybook開発: UIカタログとテストを育てる実践ガイド
Claude CodeでStorybookの設定、stories、interaction test、CI、Chromatic導入まで実装する実践手順。
Storybookは「見た目の置き場」ではなくUIの契約書
Storybookは、ReactやVueなどのUIコンポーネントをアプリ本体から切り離して表示し、状態ごとに確認できる開発環境です。初心者向けに言うと、「ボタン、フォーム、カードを単体で開ける部品カタログ」です。ただし実務で価値が出るのは、きれいな一覧を作った時ではありません。props、表示状態、操作、アクセシビリティ、デザインレビュー、CIの確認までを一つの契約として残した時です。
Claude Codeに「Storybookを作って」とだけ頼むと、Button.stories.tsxを数個生成して終わることがあります。これでは運用で使えません。必要なのは、どのコンポーネントを対象にするか、どの状態を必ず載せるか、play関数で何を検証するか、Chromaticのようなビジュアル回帰検証をPRでどう扱うかまで決めることです。
この記事では、Storybook 8/9以降でも古く見えにくい書き方として、React + TypeScript + Viteを前提に、.storybook/main.ts、CSFのMeta/StoryObj、satisfies、@storybook/testのinteraction test、@storybook/addon-vitest、Chromatic相当のCIまでつなげます。公式の流れはStorybook main configuration、TypeScript stories、Interaction tests、Vitest addon、Chromatic for Storybookを確認しています。
関連する基礎はClaude Codeデザインシステム構築とClaude Codeアクセシビリティ対応も合わせて読むと、Storybookを単なる画面集ではなく、設計と品質保証の足場にできます。
先に決める実例3つ
最初に対象を絞ります。Storybookは全部のUIを一気に載せるより、変更頻度が高く、壊れると収益や問い合わせに近い場所から整備する方が効果的です。
| 実例 | Storybookに載せる状態 | Claude Codeに任せる作業 |
|---|---|---|
| デザインシステムのButton | primary、secondary、loading、disabled、全サイズ | propsの型、stories、controls、autodocs |
| 無料登録フォーム | 入力前、入力エラー、送信成功、送信中 | play関数、mock、バリデーション確認 |
| 記事末尾や商品ページのCTA | 通常、強調、モバイル幅、長い文言 | レスポンシブ確認、Chromatic差分、内部リンク点検 |
この3つだけでも、デザイナーは状態をレビューでき、エンジニアはpropsの使い方を確認でき、Masaのようなサイト運営者は収益導線のCTAが壊れていないかをPRごとに見られます。
セットアップはVite前提で固定する
Storybook 9系では、Vitest addonを使うならViteベースのStorybook frameworkが前提になります。Next.jsの場合も@storybook/nextjs-viteを検討します。React + 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
package.jsonには、ローカル確認、静的ビルド、Storybook由来のテストを分けておきます。
{
"scripts": {
"dev": "vite",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"test-storybook": "vitest --project=storybook --run",
"chromatic": "chromatic"
}
}
.storybook/main.tsは、storiesの場所、addon、frameworkを明示します。古い記事では@storybook/reactやWebpack前提の設定が混ざりがちですが、Viteプロジェクトでは@storybook/react-viteに揃えます。
// .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;
共通のpreviewでは、Controlsの推論、a11y、autodocsを整えます。autodocsは「自動で全部のドキュメントが完璧になる魔法」ではなく、storiesとprops情報からドキュメントページを作る仕組みです。
// .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;
実例1: Buttonをコピペで動くstoriesにする
まずは小さなButtonを作ります。ここで大切なのは、Storybook専用の架空propsを作らないことです。アプリで使う本物のpropsをstoriesに出します。
// 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>
);
}
storiesはCSFで書きます。satisfies Meta<typeof Button>を使うと、存在しないpropsをargsに書いた時にTypeScriptが止めてくれます。
// 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;
Claude Codeには「全バリエーションを出して」だけでなく、「アプリで使うprops以外を増やさない」「長いCTA文言でも崩れない」「disabledとloadingのアクセシビリティ属性も確認する」と頼むと、生成物が実務に近づきます。
実例2: 登録フォームをinteraction testまで書く
Storybookのplay関数は、storyが描画された後にユーザー操作を再現する関数です。つまり、見た目だけでなく「入力して送信できるか」までstoryの中で確認できます。
// 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;
この実例は、記事CTAや無料PDF登録フォームにもそのまま応用できます。収益導線に近いUIは、見た目の差分だけでなく、入力、送信、エラー表示、成功メッセージまで確認する価値があります。
実例3: Vitest addonとChromatic相当のCIをつなぐ
Storybook 9の公式ドキュメントでは、Vite系プロジェクトならVitest addonでstoriesをコンポーネントテストとして走らせる流れが案内されています。play関数があるstoryは、描画確認に加えて操作とassertionも実行されます。
// .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 Actionsでは、テスト、静的ビルド、Chromaticアップロードを分けます。Chromaticを使わない場合でも、build-storybookを必ずCIに入れるだけで、MDXやstoriesの壊れ方をかなり早く見つけられます。
# .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 }}
Claude Codeへの依頼テンプレート
Claude Codeには、完成形ではなく検証条件を渡します。次のように頼むと、storiesだけ増やす作業から、UI品質を守る作業に変わります。
React + TypeScript + ViteのプロジェクトでStorybookを整備してください。
対象:
- src/components/Button.tsx
- src/components/SignupForm.tsx
要件:
- .storybook/main.tsは@storybook/react-viteを使う
- CSFはMeta/StoryObjとsatisfiesで型安全にする
- Buttonはprimary, secondary, outline, loading, disabled, all sizesをstories化する
- SignupFormはinvalid emailとsuccessful submitのplay関数を書く
- @storybook/testのwithin, userEvent, expect, fnを使う
- Vitest addonでtest-storybookがCI実行できる設定を追加する
- build-storybookとChromatic相当のvisual checkをGitHub Actionsに入れる
禁止:
- 疑似コードで済ませない
- 実アプリにないpropsをStorybook専用に追加しない
- secretsをコードに直書きしない
- 対象外ファイルを勝手に整形しない
完了時:
- 変更ファイル一覧
- 実行したコマンド
- 残ったリスク
を短く報告してください。
具体的な落とし穴
1つ目は、Storybook用のダミーpropsを増やすことです。toneForStorybookのようなpropsが入ると、本番コードとstoriesの契約がずれます。必要なら本番propsを見直します。
2つ目は、happy pathだけのplay関数です。送信成功だけでは不十分です。入力エラー、disabled、loading、二重送信、長い文言も最低限入れます。
3つ目は、アニメーションや時刻でChromatic差分が毎回出ることです。日付、乱数、ローディングの骨組み、外部画像は固定しないと、PRレビューで本当のUI変更が埋もれます。
4つ目は、@storybook/react、@storybook/react-vite、@storybook/nextjs-viteを混ぜることです。frameworkがずれると、addon-vitestやVite aliasの解決で詰まります。Claude Codeには最初にframework名を明示します。
5つ目は、CIでブラウザを入れ忘れることです。Vitest browser modeやPlaywrightを使うなら、CIにnpx playwright install --with-deps chromiumを入れます。
6つ目は、収益CTAのstoriesを後回しにすることです。デザインシステムのButtonだけ整っていても、記事末尾のCTA、商品カード、問い合わせフォームが壊れていれば売上導線は守れません。
収益導線としてのStorybook
ClaudeCodeLabのような技術メディアでは、Storybookは開発者だけの道具ではありません。無料PDFの登録フォーム、教材ページの購入CTA、研修相談フォーム、記事末尾の内部リンクカードをstory化しておくと、記事更新やデザイン変更のたびに「押せるか」「読めるか」「崩れていないか」を確認できます。
まず個人で試すなら無料チートシートでClaude Codeへの依頼の型を固めるのが早いです。テンプレート化してチームに配るなら教材・テンプレート一覧へ進んでください。実リポジトリでStorybook、CI、Chromatic、アクセシビリティ、収益CTAまでまとめて整えたい場合はClaude Code研修・導入相談が現実的です。
この記事で紹介した内容を実際に試した結果
Masaが小さなReact構成で試した時、最も手戻りが減ったのはButtonのバリエーションではなく、SignupFormの失敗storyでした。成功だけを確認していた時は、メール形式エラーと二重送信の見落としが後から出ました。play関数でinvalid emailとsuccessful submitを分け、CIでtest-storybookとbuild-storybookを走らせると、Claude Codeに追加修正を頼む回数が減ります。Chromatic相当の確認も、全ページのスクリーンショットより、収益CTAとフォームに絞った方がレビューしやすいと感じました。Storybookは飾りではなく、UIの状態、操作、収益導線を同じ場所で守るための実務ツールとして使うのが一番効果的です。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
ObsidianメモをCLAUDE.mdに変えるClaude Code運用: 文脈を毎回説明しない仕組み
Obsidianの作業メモからCLAUDE.md用の運用ノートを作り、Claude Codeに安定した文脈を渡す方法。
Claude Code Revenue CTA Routing: 記事からPDF、Gumroad、相談へ送る設計
PVだけで終わらせず、読者の状態に合わせて無料PDF、Gumroad教材、導入相談へ分岐するCTA設計です。
Claude Codeチーム引き継ぎルール: レビュー、権限、収益導線まで渡す実務手順
Claude Codeの作業をチームで渡すための証拠、権限、ロールバック、無料PDF/Gumroad/相談導線の実務ルール。