UI コンポーネントを Figma で眺めるだけでなく、ブラウザ上で隔離実行・ドキュメント化・自動テスト・Visual Regression まで一気通貫で回せるのが Storybook です。本記事では Storybook 8.x(2026年5月時点)を前提に、インストール → CSF 3.0 → autodocs → interaction tests → Visual Regression(Chromatic / Loki)→ CI/CD 公開まで、コピペで動く JS / TS コード 40+ ブロックで実装手順を追います。
本稿の対象は「Storybook を本気で実務投入したい人」「play function による interaction test を CI に組み込みたい人」「React Testing Library との使い分けに迷っている人」です。読み終える頃にはカラーパレットからフォーム挙動まで Storybook 1 箇所で確認・テスト・公開できる状態になっているはずです。
- 1. Storybook とは何か(隔離開発環境 + Component Workshop)
- 2. インストールとプロジェクト初期化
- 3. main.ts と preview.ts(最重要設定 2 ファイル)
- 4. CSF 3.0 で Story を書く(Meta / StoryObj 型)
- 5. autodocs と MDX で「使えるドキュメント」を量産する
- 6. addon-essentials と主要 addon の使い倒し
- 7. play function による interaction test(Storybook 内テスト)
- 8. test-runner と CI(Storybook をテストランナーで叩く)
- 9. Visual Regression(Chromatic / Loki / Percy)
- 10. フレームワーク別セットアップ(React / Vue / Svelte / Angular)
- 11. Decorator で「現実のアプリと同じ環境」を作る
- 12. MSW(Mock Service Worker)で API をモックする
- 13. CI/CD と GitHub Pages 公開
- 14. Storybook と Figma / デザインシステム連携
- 15. Storybook と React Testing Library の使い分け
- 16. まとめ — Storybook 8 を「使える開発資産」にする 7 か条
1. Storybook とは何か(隔離開発環境 + Component Workshop)
Storybook は 2016 年にリリースされた UI コンポーネント単位の隔離開発環境です。アプリ全体を起動しなくても、1 つのボタンやフォームを http://localhost:6006 で叩き、props を GUI で切り替えながら見た目と挙動を確認できます。8.x 系では以下が標準装備になりました。
- CSF 3.0: Story を「ただのオブジェクト」として書く新形式(
Meta/StoryObj型) - autodocs: 1 行設定で MDX 風のドキュメントページを自動生成
- play function: Story 内で
userEventを呼び interaction test を実装(Testing Library 互換 API) - test-runner: Playwright で全 Story を smoke test / a11y test として CI 実行
- Vite Builder 既定化: Webpack5 Builder は opt-in に格下げ
1.1 動作要件
# Node.js 18.0+ / 20+ / 22+ が必須(Storybook 8.x)
node -v
# v22.14.0
# パッケージマネージャ(npm / pnpm / yarn / bun いずれも可)
npm -v
pnpm -v
1.2 対応フレームワーク
# Storybook 8 が公式サポートする framework パッケージ
@storybook/react-vite
@storybook/react-webpack5
@storybook/nextjs
@storybook/vue3-vite
@storybook/svelte-vite
@storybook/sveltekit
@storybook/angular
@storybook/web-components-vite
@storybook/html-vite
2. インストールとプロジェクト初期化
2.1 既存プロジェクトに導入(最速ルート)
# 既存の Vite / Next / Nuxt プロジェクトのルートで実行
npx storybook@latest init
# CI 用の非対話モード
npx storybook@latest init --yes --skip-install
# 後で手動で依存を入れる場合
pnpm install
storybook init はプロジェクトの package.json を解析し、React なら @storybook/react-vite、Vue なら @storybook/vue3-vite、Next.js なら @storybook/nextjs を自動選定します。
2.2 ゼロから React + Vite + TS で立ち上げる
# Vite テンプレート作成
npm create vite@latest my-ui -- --template react-ts
cd my-ui
npm install
# Storybook 導入
npx storybook@latest init
# 起動(自動でブラウザが http://localhost:6006 を開く)
npm run storybook
2.3 生成されるディレクトリ
# Storybook init 直後のツリー
my-ui/
├─ .storybook/
│ ├─ main.ts # アドオン・stories パス・builder 設定
│ └─ preview.ts # グローバル decorator / parameters / globalTypes
├─ src/
│ └─ stories/ # サンプル Story 一式(Button / Header / Page)
└─ package.json # storybook / build-storybook スクリプト
3. main.ts と preview.ts(最重要設定 2 ファイル)
3.1 main.ts(プロジェクト設定)
// .storybook/main.ts
import type { StorybookConfig } from "@storybook/react-vite";
const config: StorybookConfig = {
stories: [
"../src/**/*.mdx",
"../src/**/*.stories.@(js|jsx|mjs|ts|tsx)",
],
addons: [
"@storybook/addon-essentials",
"@storybook/addon-interactions",
"@storybook/addon-a11y",
"@storybook/addon-themes",
"@storybook/addon-coverage",
],
framework: {
name: "@storybook/react-vite",
options: {},
},
docs: {
autodocs: "tag", // tag:["autodocs"] が付いた Story だけ自動ドキュメント化
},
staticDirs: ["../public"],
typescript: {
reactDocgen: "react-docgen-typescript",
reactDocgenTypescriptOptions: {
shouldExtractLiteralValuesFromEnum: true,
propFilter: (prop) =>
prop.parent ? !/node_modules/.test(prop.parent.fileName) : true,
},
},
};
export default config;
3.2 preview.ts(全 Story に効くグローバル設定)
// .storybook/preview.ts
import type { Preview } from "@storybook/react";
import "../src/index.css"; // Tailwind や global CSS を読み込む
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
backgrounds: {
default: "light",
values: [
{ name: "light", value: "#ffffff" },
{ name: "dark", value: "#0f172a" },
],
},
layout: "centered",
},
tags: ["autodocs"],
};
export default preview;
3.3 Vite Builder のカスタマイズ
// .storybook/main.ts に viteFinal を追加
import { mergeConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
const config: StorybookConfig = {
// ...省略
async viteFinal(config) {
return mergeConfig(config, {
plugins: [tsconfigPaths()],
resolve: {
alias: {
"@": "/src",
},
},
});
},
};
3.4 Webpack 5 Builder へ切り替えたい場合
# 既存プロジェクトを Webpack Builder で初期化
npx storybook@latest init --builder webpack5
# 手動切替
pnpm add -D @storybook/react-webpack5
// .storybook/main.ts
import type { StorybookConfig } from "@storybook/react-webpack5";
const config: StorybookConfig = {
framework: { name: "@storybook/react-webpack5", options: {} },
webpackFinal: async (config) => {
config.resolve!.alias = { ...config.resolve!.alias, "@": "/src" };
return config;
},
};
export default config;
4. CSF 3.0 で Story を書く(Meta / StoryObj 型)
4.1 基本コンポーネント(Button)
// src/components/Button.tsx
import type { ButtonHTMLAttributes, FC } from "react";
export type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
variant?: "primary" | "secondary" | "danger";
size?: "sm" | "md" | "lg";
loading?: boolean;
};
export const Button: FC<ButtonProps> = ({
variant = "primary",
size = "md",
loading,
children,
...rest
}) => {
const cls = `btn btn-${variant} btn-${size}${loading ? " is-loading" : ""}`;
return (
<button className={cls} disabled={loading || rest.disabled} {...rest}>
{loading ? "Loading..." : children}
</button>
);
};
4.2 CSF 3.0 形式の Story
// src/components/Button.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import { Button } from "./Button";
const meta = {
title: "UI/Button",
component: Button,
tags: ["autodocs"],
argTypes: {
variant: {
control: "select",
options: ["primary", "secondary", "danger"],
},
size: { control: "radio", options: ["sm", "md", "lg"] },
loading: { control: "boolean" },
onClick: { action: "clicked" },
},
args: { children: "ボタン", variant: "primary", size: "md" },
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {};
export const Secondary: Story = { args: { variant: "secondary" } };
export const Danger: Story = { args: { variant: "danger" } };
export const Loading: Story = { args: { loading: true } };
export const Disabled: Story = { args: { disabled: true } };
4.3 satisfies を使う意義
satisfies Meta<typeof Button> と書くことで、args の型が ButtonProps に完全推論されつつ、meta 自身は Meta 型のサブセットとして縛られます。as Meta よりも型情報が落ちず、Story["args"] の補完が効きます。
4.4 render を使ったカスタム描画
// 複数コンポーネント並べたいときは render を上書き
export const Group: Story = {
render: (args) => (
<div style={{ display: "flex", gap: 8 }}>
<Button {...args} variant="primary">Primary</Button>
<Button {...args} variant="secondary">Secondary</Button>
<Button {...args} variant="danger">Danger</Button>
</div>
),
};
4.5 旧 CSF 2.0 との対比
// ❌ CSF 2.0(関数 Story + bind)
// export const Primary = Template.bind({});
// Primary.args = { variant: "primary" };
// ✅ CSF 3.0(オブジェクト Story)
export const Primary: Story = { args: { variant: "primary" } };
5. autodocs と MDX で「使えるドキュメント」を量産する
5.1 autodocs を有効化
// .storybook/main.ts
docs: { autodocs: "tag" },
// 各 Story 側
const meta = {
title: "UI/Button",
component: Button,
tags: ["autodocs"], // ← これが付いた Story だけ Docs ページ生成
} satisfies Meta<typeof Button>;
5.2 JSDoc がそのまま description になる
// src/components/Button.tsx
export type ButtonProps = {
/** 配色バリアント。CTAは primary、破壊操作は danger を選択 */
variant?: "primary" | "secondary" | "danger";
/** ボタンサイズ。フォーム内は md、ヒーローは lg を推奨 */
size?: "sm" | "md" | "lg";
};
5.3 MDX で手書きドキュメントを追加する
// src/components/Button.mdx
import { Meta, Canvas, Controls, Story } from "@storybook/blocks";
import * as ButtonStories from "./Button.stories";
<Meta of={ButtonStories} />
# Button
社内デザインシステムの**最重要 CTA コンポーネント**。配色は Tailwind の `primary-600` を基調とします。
## バリアント一覧
<Canvas of={ButtonStories.Primary} />
<Canvas of={ButtonStories.Secondary} />
<Canvas of={ButtonStories.Danger} />
## Props
<Controls of={ButtonStories.Primary} />
5.4 README をそのまま読み込む
// src/components/Button.mdx
import { Meta } from "@storybook/blocks";
import Readme from "./README.md?raw";
<Meta title="UI/Button/README" />
<pre>{Readme}</pre>
6. addon-essentials と主要 addon の使い倒し
6.1 essentials に含まれるもの
# @storybook/addon-essentials の中身(8.x)
- actions # onClick などのイベントログ
- backgrounds # 背景色切替
- controls # args をGUIで編集
- docs # autodocs / MDX
- highlight # 要素ハイライト
- measure # 要素サイズ計測
- outline # CSSアウトライン表示
- toolbars # ツールバーUI
- viewport # 画面サイズ切替
6.2 addon-actions(イベントを Actions パネルに流す)
// 2 通りの記法
import { action } from "@storybook/addon-actions";
// (A) argTypes で自動収集
argTypes: { onClick: { action: "clicked" } }
// (B) 明示的に呼ぶ
export const WithCustomAction: Story = {
args: { onClick: action("custom-click") },
};
6.3 addon-controls(args の GUI 編集)
argTypes: {
variant: { control: "select", options: ["primary","secondary","danger"] },
size: { control: { type: "range", min: 8, max: 64, step: 4 } },
color: { control: "color" },
birth: { control: "date" },
payload: { control: "object" },
enabled: { control: "boolean" },
}
6.4 addon-viewport(レスポンシブ確認)
// preview.ts に追記
import { INITIAL_VIEWPORTS } from "@storybook/addon-viewport";
parameters: {
viewport: {
viewports: {
...INITIAL_VIEWPORTS,
custom: {
name: "Custom 1440",
styles: { width: "1440px", height: "900px" },
type: "desktop",
},
},
defaultViewport: "iphone14",
},
},
6.5 addon-a11y(axe-core を内蔵)
# インストール
pnpm add -D @storybook/addon-a11y
// main.ts
addons: ["@storybook/addon-a11y"],
// 個別 Story で違反を無視
export const KnownIssue: Story = {
parameters: {
a11y: {
config: { rules: [{ id: "color-contrast", enabled: false }] },
},
},
};
6.6 addon-themes(ダーク / ライトの切替)
// preview.ts
import { withThemeByClassName } from "@storybook/addon-themes";
export const decorators = [
withThemeByClassName({
themes: { light: "theme-light", dark: "theme-dark" },
defaultTheme: "light",
}),
];
6.7 addon-coverage(Story カバレッジ計測)
pnpm add -D @storybook/addon-coverage
# vitest と組み合わせて Story 経由のカバレッジを取得
7. play function による interaction test(Storybook 内テスト)
7.1 シンプルな click テスト
// src/components/Button.stories.tsx
import { userEvent, within, expect, fn } from "@storybook/test";
export const Clicked: Story = {
args: { onClick: fn() },
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const btn = canvas.getByRole("button", { name: /ボタン/ });
await userEvent.click(btn);
await expect(args.onClick).toHaveBeenCalledTimes(1);
},
};
7.2 フォーム入力フロー
// src/components/LoginForm.stories.tsx
export const HappyPath: Story = {
play: async ({ canvasElement }) => {
const c = within(canvasElement);
await userEvent.type(c.getByLabelText("メール"), "user@example.com");
await userEvent.type(c.getByLabelText("パスワード"), "P@ssw0rd!");
await userEvent.click(c.getByRole("button", { name: /ログイン/ }));
await expect(await c.findByText(/ようこそ/)).toBeInTheDocument();
},
};
7.3 失敗ケース(バリデーションエラー)
export const ValidationError: Story = {
play: async ({ canvasElement }) => {
const c = within(canvasElement);
await userEvent.click(c.getByRole("button", { name: /ログイン/ }));
await expect(c.getByText(/メールは必須です/)).toBeVisible();
await expect(c.getByText(/パスワードは必須です/)).toBeVisible();
},
};
7.4 step() でテストを区切る
export const MultiStep: Story = {
play: async ({ canvasElement, step }) => {
const c = within(canvasElement);
await step("メール入力", async () => {
await userEvent.type(c.getByLabelText("メール"), "a@b.c");
});
await step("送信", async () => {
await userEvent.click(c.getByRole("button"));
});
await step("完了表示", async () => {
await expect(await c.findByText(/送信しました/)).toBeVisible();
});
},
};
7.5 @storybook/test と Testing Library の違い
// ❌ 8.0 以前は @storybook/testing-library を import していた
// import { userEvent, within } from "@storybook/testing-library";
// ✅ 8.x は @storybook/test に統合(Vitest 互換 expect も同梱)
import { userEvent, within, expect, fn, waitFor } from "@storybook/test";
8. test-runner と CI(Storybook をテストランナーで叩く)
8.1 test-runner 導入
pnpm add -D @storybook/test-runner
# Playwright のブラウザを取得
npx playwright install --with-deps chromium
// package.json
{
"scripts": {
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"test-storybook": "test-storybook",
"test-storybook:ci": "concurrently -k -s first -n SB,TEST "npm:storybook -- --ci --quiet" "wait-on tcp:6006 && test-storybook""
}
}
8.2 全 Story を Smoke Test として実行
# ローカル(Storybook が 6006 で起動している前提)
npm run test-storybook
# CI 用(Storybook 起動を待ってからテスト)
npm run test-storybook:ci
8.3 a11y チェックを test-runner に組み込む
// .storybook/test-runner.ts
import { getStoryContext } from "@storybook/test-runner";
import { injectAxe, checkA11y } from "axe-playwright";
export default {
async preVisit(page) { await injectAxe(page); },
async postVisit(page, context) {
const story = await getStoryContext(page, context);
if (story.parameters?.a11y?.disable) return;
await checkA11y(page, "#storybook-root", {
detailedReport: true,
detailedReportOptions: { html: true },
});
},
};
8.4 GitHub Actions ワークフロー
# .github/workflows/storybook.yml
name: Storybook Tests
on: [push, pull_request]
jobs:
storybook-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with: { version: 9 }
- uses: actions/setup-node@v4
with: { node-version: 22, cache: pnpm }
- run: pnpm install --frozen-lockfile
- run: npx playwright install --with-deps chromium
- run: pnpm build-storybook --quiet
- run: pnpm dlx http-server storybook-static -p 6006 &
- run: pnpm dlx wait-on tcp:6006
- run: pnpm test-storybook
9. Visual Regression(Chromatic / Loki / Percy)
9.1 Chromatic(Storybook 公式・最速ルート)
pnpm add -D chromatic
# 初回:Chromatic にプロジェクト作成 & トークン発行
npx chromatic --project-token=<your-token>
# 2 回目以降は GitHub Actions に CHROMATIC_PROJECT_TOKEN を渡すだけ
npx chromatic --exit-zero-on-changes
9.2 Chromatic を GitHub Actions に組み込む
# .github/workflows/chromatic.yml
name: Chromatic
on: [push]
jobs:
chromatic:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 } # ベースライン比較に必須
- uses: actions/setup-node@v4
with: { node-version: 22 }
- run: npm ci
- uses: chromaui/action@latest
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
onlyChanged: true
9.3 Loki(セルフホスト Visual Regression)
pnpm add -D loki
# 初期化
npx loki init
# 起動中の Storybook に対して画像比較
npx loki test
// package.json(抜粋)
"loki": {
"configurations": {
"chrome.laptop": { "target": "chrome.docker", "width": 1366, "height": 768 },
"chrome.iphone7": { "target": "chrome.docker", "preset": "iPhone 7" }
}
}
9.4 Percy(BrowserStack 系 SaaS)
pnpm add -D @percy/cli @percy/storybook
PERCY_TOKEN=xxx npx percy storybook ./storybook-static
9.5 どれを選ぶか
# 結論(2026年版)
- スピード優先 / 個人〜中規模: Chromatic(無料枠厚い)
- 完全セルフホスト必須(金融・医療):Loki
- 他の Percy 製品と統合済み: Percy
10. フレームワーク別セットアップ(React / Vue / Svelte / Angular)
10.1 React + Next.js
npx storybook@latest init
# Next.js が検出されると @storybook/nextjs が自動選定される
// .storybook/main.ts
import type { StorybookConfig } from "@storybook/nextjs";
const config: StorybookConfig = {
framework: {
name: "@storybook/nextjs",
options: { nextConfigPath: "../next.config.mjs" },
},
};
export default config;
10.2 Next.js Image / Link の自動 mock
// @storybook/nextjs は next/image / next/link / next/navigation を
// 自動的に Storybook 互換に差し替える。追加設定は不要。
// router をモックしたい場合
parameters: {
nextjs: {
router: { pathname: "/profile", query: { id: "1" } },
appDirectory: true,
},
},
10.3 Vue 3 + Vite
// src/components/Hello.stories.ts
import type { Meta, StoryObj } from "@storybook/vue3";
import Hello from "./Hello.vue";
const meta = {
title: "UI/Hello",
component: Hello,
tags: ["autodocs"],
argTypes: { name: { control: "text" } },
} satisfies Meta<typeof Hello>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { args: { name: "Storybook" } };
10.4 Svelte 5
// src/components/Counter.stories.ts
import type { Meta, StoryObj } from "@storybook/svelte";
import Counter from "./Counter.svelte";
const meta = { title: "UI/Counter", component: Counter,
tags: ["autodocs"] } satisfies Meta<Counter>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { args: { initial: 0 } };
10.5 Angular
// src/app/button.stories.ts
import type { Meta, StoryObj } from "@storybook/angular";
import { ButtonComponent } from "./button.component";
const meta: Meta<ButtonComponent> = {
title: "UI/Button",
component: ButtonComponent,
tags: ["autodocs"],
};
export default meta;
type Story = StoryObj<ButtonComponent>;
export const Primary: Story = { args: { variant: "primary" } };
11. Decorator で「現実のアプリと同じ環境」を作る
11.1 ThemeProvider / Router を全 Story に適用
// .storybook/preview.tsx
import { MemoryRouter } from "react-router-dom";
import { ThemeProvider } from "@/lib/theme";
export const decorators = [
(Story) => (
<ThemeProvider theme="light">
<MemoryRouter><Story /></MemoryRouter>
</ThemeProvider>
),
];
11.2 個別 Story だけに Decorator を当てる
export const Authenticated: Story = {
decorators: [
(Story) => (
<AuthContext.Provider value={{ user: { id: 1, name: "山田" } }}>
<Story />
</AuthContext.Provider>
),
],
};
11.3 Tailwind CSS を組み込む
// .storybook/preview.ts
import "../src/styles/globals.css"; // Tailwind の @tailwind base/components/utilities を含む CSS
// tailwind.config.js
export default {
content: [
"./src/**/*.{js,ts,jsx,tsx,mdx}",
"./.storybook/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: { extend: {} },
};
11.4 React Query / TanStack Query の Provider
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false, staleTime: Infinity } },
});
export const decorators = [
(Story) => (
<QueryClientProvider client={queryClient}>
<Story />
</QueryClientProvider>
),
];
12. MSW(Mock Service Worker)で API をモックする
12.1 インストール
pnpm add -D msw msw-storybook-addon
npx msw init public --save
12.2 preview.ts に Loader を仕込む
// .storybook/preview.ts
import { initialize, mswLoader } from "msw-storybook-addon";
initialize();
const preview: Preview = {
loaders: [mswLoader],
parameters: {},
};
export default preview;
12.3 Story 単位でハンドラを定義
// src/components/UserCard.stories.tsx
import { http, HttpResponse } from "msw";
export const Success: Story = {
parameters: {
msw: {
handlers: [
http.get("/api/users/1", () =>
HttpResponse.json({ id: 1, name: "山田太郎" })),
],
},
},
};
export const Error500: Story = {
parameters: {
msw: {
handlers: [
http.get("/api/users/1", () =>
new HttpResponse(null, { status: 500 })),
],
},
},
};
13. CI/CD と GitHub Pages 公開
13.1 build-storybook で静的サイト出力
pnpm build-storybook
# storybook-static/ に静的ファイルが生成される
ls storybook-static/
# index.html iframe.html assets/ sb-manager/ sb-preview/
13.2 GitHub Pages にデプロイ
# .github/workflows/deploy-storybook.yml
name: Deploy Storybook to Pages
on:
push: { branches: [main] }
permissions:
contents: read
pages: write
id-token: write
jobs:
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22 }
- run: npm ci
- run: npm run build-storybook
- uses: actions/upload-pages-artifact@v3
with: { path: storybook-static }
- id: deployment
uses: actions/deploy-pages@v4
13.3 Vercel / Netlify にデプロイ
# Vercel
vercel --prod --build-env NPM_FLAGS="--legacy-peer-deps"
--build-command "npm run build-storybook"
--output-directory "storybook-static"
# Netlify(netlify.toml)
# [build]
# command = "npm run build-storybook"
# publish = "storybook-static"
13.4 PR ごとに Preview URL を出す(Chromatic 一石二鳥)
# Chromatic はビルド毎にユニーク URL を発行する。
# PR のチェックに「View Storybook」のリンクが自動表示される。
npx chromatic --exit-once-uploaded --auto-accept-changes=main
14. Storybook と Figma / デザインシステム連携
14.1 addon-designs(旧 design-addon)
pnpm add -D @storybook/addon-designs
// 各 Story に Figma URL を紐づける
export const Primary: Story = {
parameters: {
design: {
type: "figma",
url: "https://www.figma.com/file/abc/Design-System?node-id=1%3A2",
},
},
};
14.2 Design Token を CSS 変数として注入
/* src/styles/tokens.css(Figma Tokens プラグインから export) */
:root {
--color-primary-600: #2563eb;
--color-danger-600: #dc2626;
--radius-md: 8px;
--spacing-md: 16px;
}
// preview.ts で読み込む
import "../src/styles/tokens.css";
14.3 Atomic Design + Storybook のディレクトリ規約
src/
├─ atoms/ # Button / Input / Icon
│ └─ Button/
│ ├─ Button.tsx
│ ├─ Button.stories.tsx
│ └─ Button.mdx
├─ molecules/ # FormField / Card
├─ organisms/ # Header / Footer / LoginForm
├─ templates/ # PageLayout
└─ pages/ # 実ページ(本番アプリと共有)
title は階層名と一致させ、Storybook サイドバーで Atomic Design のツリーがそのまま見えるようにします(例: title: "Atoms/Button")。
15. Storybook と React Testing Library の使い分け
本サイト既存記事「React Testing Library 完全実践ガイド」と読み合わせる方も多いはずです。Storybook の interaction test と RTL は競合しません。次のように役割を分けます。
15.1 役割分担マトリクス
# 見た目 / 振る舞いの境界
- 見た目だけ(配色・余白・タイポ) → Storybook + Chromatic
- 1 コンポーネント単位の振る舞い → Storybook play function
- 複数コンポーネントの統合振る舞い → React Testing Library(RTL)
- ルーティング / SSR / API 結線 → RTL + MSW
- E2E(ログイン〜決済まで) → Playwright
15.2 同じテストを RTL と play function で書いた比較
// (A) RTL(Vitest)
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Button } from "./Button";
test("click", async () => {
const onClick = vi.fn();
render(<Button onClick={onClick}>OK</Button>);
await userEvent.click(screen.getByRole("button"));
expect(onClick).toHaveBeenCalled();
});
// (B) Storybook play function(同じ assertion を Storybook 内で実行)
export const Click: Story = {
args: { onClick: fn(), children: "OK" },
play: async ({ canvasElement, args }) => {
const c = within(canvasElement);
await userEvent.click(c.getByRole("button"));
await expect(args.onClick).toHaveBeenCalled();
},
};
同じ assertion ですが、(B) は ブラウザで実際に描画した上でテストでき、しかも開発者が UI を目視レビューする画面と同居します。「壊れたときにスクショで即理解できる」のが Storybook 側の最大の利点です。
15.3 推奨運用
# 実務での回し方(2026年版)
1. UI を作るときは Storybook で先に Story を書く(TDD的に play も書く)
2. 統合や非UI ロジック(hooks 単体・ユーティリティ)は Vitest + RTL
3. CI では `test-storybook`(全Story smoke) と `vitest run` を並列実行
4. Visual Regression は Chromatic を PR 必須チェックにする
5. リリース前に Playwright で E2E を流す(checkout・決済のみで OK)
16. まとめ — Storybook 8 を「使える開発資産」にする 7 か条
- CSF 3.0 + satisfies: Story はオブジェクトで書き、型を最大限効かせる
- autodocs: JSDoc を充実させ、ドキュメントを自動生成する
- play function: Story と一緒に interaction test を書き、CI で
test-storybook - MSW: 成功・失敗・空・ローディングの 4 状態を必ず Story として残す
- Chromatic: PR ごとに Visual Regression を必須チェック化
- Decorator: Theme / Router / QueryClient はグローバル decorator で集中管理
- RTL との分業: 見た目と単体振る舞いは Storybook、統合は RTL、E2E は Playwright
Storybook を「Story を書く場所」だけにしておくのは勿体ない使い方です。ドキュメント・interaction test・Visual Regression・デザインレビュー・公開デモ の 5 役を 1 つのビルドで賄える点こそ 8.x 系の真価です。本記事のコード片を .storybook/ と src/**/*.stories.tsx にそのまま落とし込めば、明日からチーム全体で「壊れない UI」を量産できる体制が整います。
関連記事として、本サイトの React Testing Library 完全実践ガイド・Vite 6 完全実践ガイド・Next.js App Router 完全ガイド を併読すると、テスト・ビルド・フレームワークの 3 軸が揃います。

コメント