Storybook 8完全実践ガイド〜CSF 3.0・autodocs・interaction tests・Visual Regression【2026年版】〜

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. 1. Storybook とは何か(隔離開発環境 + Component Workshop)
    1. 1.1 動作要件
    2. 1.2 対応フレームワーク
  2. 2. インストールとプロジェクト初期化
    1. 2.1 既存プロジェクトに導入(最速ルート)
    2. 2.2 ゼロから React + Vite + TS で立ち上げる
    3. 2.3 生成されるディレクトリ
  3. 3. main.ts と preview.ts(最重要設定 2 ファイル)
    1. 3.1 main.ts(プロジェクト設定)
    2. 3.2 preview.ts(全 Story に効くグローバル設定)
    3. 3.3 Vite Builder のカスタマイズ
    4. 3.4 Webpack 5 Builder へ切り替えたい場合
  4. 4. CSF 3.0 で Story を書く(Meta / StoryObj 型)
    1. 4.1 基本コンポーネント(Button)
    2. 4.2 CSF 3.0 形式の Story
    3. 4.3 satisfies を使う意義
    4. 4.4 render を使ったカスタム描画
    5. 4.5 旧 CSF 2.0 との対比
  5. 5. autodocs と MDX で「使えるドキュメント」を量産する
    1. 5.1 autodocs を有効化
    2. 5.2 JSDoc がそのまま description になる
    3. 5.3 MDX で手書きドキュメントを追加する
    4. 5.4 README をそのまま読み込む
  6. 6. addon-essentials と主要 addon の使い倒し
    1. 6.1 essentials に含まれるもの
    2. 6.2 addon-actions(イベントを Actions パネルに流す)
    3. 6.3 addon-controls(args の GUI 編集)
    4. 6.4 addon-viewport(レスポンシブ確認)
    5. 6.5 addon-a11y(axe-core を内蔵)
    6. 6.6 addon-themes(ダーク / ライトの切替)
    7. 6.7 addon-coverage(Story カバレッジ計測)
  7. 7. play function による interaction test(Storybook 内テスト)
    1. 7.1 シンプルな click テスト
    2. 7.2 フォーム入力フロー
    3. 7.3 失敗ケース(バリデーションエラー)
    4. 7.4 step() でテストを区切る
    5. 7.5 @storybook/test と Testing Library の違い
  8. 8. test-runner と CI(Storybook をテストランナーで叩く)
    1. 8.1 test-runner 導入
    2. 8.2 全 Story を Smoke Test として実行
    3. 8.3 a11y チェックを test-runner に組み込む
    4. 8.4 GitHub Actions ワークフロー
  9. 9. Visual Regression(Chromatic / Loki / Percy)
    1. 9.1 Chromatic(Storybook 公式・最速ルート)
    2. 9.2 Chromatic を GitHub Actions に組み込む
    3. 9.3 Loki(セルフホスト Visual Regression)
    4. 9.4 Percy(BrowserStack 系 SaaS)
    5. 9.5 どれを選ぶか
  10. 10. フレームワーク別セットアップ(React / Vue / Svelte / Angular)
    1. 10.1 React + Next.js
    2. 10.2 Next.js Image / Link の自動 mock
    3. 10.3 Vue 3 + Vite
    4. 10.4 Svelte 5
    5. 10.5 Angular
  11. 11. Decorator で「現実のアプリと同じ環境」を作る
    1. 11.1 ThemeProvider / Router を全 Story に適用
    2. 11.2 個別 Story だけに Decorator を当てる
    3. 11.3 Tailwind CSS を組み込む
    4. 11.4 React Query / TanStack Query の Provider
  12. 12. MSW(Mock Service Worker)で API をモックする
    1. 12.1 インストール
    2. 12.2 preview.ts に Loader を仕込む
    3. 12.3 Story 単位でハンドラを定義
  13. 13. CI/CD と GitHub Pages 公開
    1. 13.1 build-storybook で静的サイト出力
    2. 13.2 GitHub Pages にデプロイ
    3. 13.3 Vercel / Netlify にデプロイ
    4. 13.4 PR ごとに Preview URL を出す(Chromatic 一石二鳥)
  14. 14. Storybook と Figma / デザインシステム連携
    1. 14.1 addon-designs(旧 design-addon)
    2. 14.2 Design Token を CSS 変数として注入
    3. 14.3 Atomic Design + Storybook のディレクトリ規約
  15. 15. Storybook と React Testing Library の使い分け
    1. 15.1 役割分担マトリクス
    2. 15.2 同じテストを RTL と play function で書いた比較
    3. 15.3 推奨運用
  16. 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 軸が揃います。

コメント

タイトルとURLをコピーしました