<CodeLearn/>
Storybook レッスン2

ストーリーの書き方

CSF3フォーマット、args、controls、decorators、play functionsを使いこなそう

CSF3(Component Story Format 3)

StorybookのストーリーはCSF3というフォーマットで記述します。 ファイルのデフォルトエクスポートがmeta(コンポーネント情報)、 名前付きエクスポートが個々のStoryオブジェクトです。

import type { Meta, StoryObj } from "@storybook/react";
import { MyComponent } from "./MyComponent";

// meta: コンポーネントの共通設定
const meta: Meta<typeof MyComponent> = {
  title: "UI/MyComponent",      // サイドバーの階層
  component: MyComponent,        // 対象コンポーネント
  tags: ["autodocs"],            // 自動ドキュメント生成
  parameters: {
    layout: "centered",          // 表示レイアウト
  },
};

export default meta;
type Story = StoryObj<typeof MyComponent>;

// 名前付きエクスポート = 個々のストーリー
export const Default: Story = {
  args: {
    // コンポーネントに渡すprops
  },
};

CSF3の主な特徴

  • TypeScriptの型推論が効き、argsの補完が効く
  • Storyオブジェクトはシンプルなオブジェクトリテラル
  • metaの設定を各Storyが継承・上書きできる
  • play functionsでインタラクションテストが可能

argsとargTypes

argsはコンポーネントに渡すpropsの値、argTypesはControls パネルでの操作方法を定義します。 TypeScriptの型情報から自動推論もされますが、明示的に設定することもできます。

// Cardコンポーネントの例
interface CardProps {
  title: string;
  description: string;
  variant: "default" | "outlined" | "elevated";
  showFooter?: boolean;
  onAction?: () => void;
}

// Card.stories.tsx
const meta: Meta<typeof Card> = {
  title: "Components/Card",
  component: Card,
  argTypes: {
    variant: {
      control: "select",              // ドロップダウンで選択
      options: ["default", "outlined", "elevated"],
      description: "カードのスタイル",
    },
    showFooter: {
      control: "boolean",             // チェックボックス
      description: "フッターの表示切替",
    },
    onAction: {
      action: "clicked",              // Actionsパネルにログ出力
    },
    title: {
      control: "text",                // テキスト入力
    },
  },
};

export default meta;
type Story = StoryObj<typeof Card>;

// metaでデフォルトargsを設定
// 各Storyで上書きできる
export const Default: Story = {
  args: {
    title: "カードタイトル",
    description: "カードの説明文がここに入ります。",
    variant: "default",
    showFooter: true,
  },
};

export const Outlined: Story = {
  args: {
    ...Default.args,
    variant: "outlined",             // variantだけ上書き
  },
};

export const WithoutFooter: Story = {
  args: {
    ...Default.args,
    showFooter: false,
  },
};

Storybook UIの「Controls」パネルで、argsの値をリアルタイムに変更してコンポーネントの挙動を確認できます。 デザイナーやPMがブラウザ上でバリエーションを確認するのに便利です。

Decorators(デコレーター)

デコレーターは、ストーリーをラップする関数です。 レイアウト調整、テーマプロバイダー、ルーターのモックなど、 コンポーネントの描画に必要な外部コンテキストを提供します。

// ストーリー単位のデコレーター
export const WithPadding: Story = {
  decorators: [
    (Story) => (
      <div style={{ padding: "3rem" }}>
        <Story />
      </div>
    ),
  ],
  args: { label: "パディング付き" },
};

// metaレベル(全ストーリー共通)のデコレーター
const meta: Meta<typeof Button> = {
  title: "Components/Button",
  component: Button,
  decorators: [
    (Story) => (
      <div className="flex gap-4 items-center">
        <Story />
      </div>
    ),
  ],
};
// preview.tsでグローバルデコレーター(全ストーリーに適用)
// .storybook/preview.ts
import { ThemeProvider } from "../src/providers/theme";

const preview: Preview = {
  decorators: [
    (Story) => (
      <ThemeProvider defaultTheme="dark">
        <Story />
      </ThemeProvider>
    ),
  ],
};

export default preview;

デコレーターの適用順序

Story単位 → meta単位 → グローバル(preview.ts)の順で内側から外側に適用されます。 最も外側にグローバルデコレーターが来るため、テーマやルーターなどの共通コンテキストはpreview.tsに設定するのが定石です。

Play Functions(インタラクションテスト)

play関数を使うと、 ストーリーが描画された後にユーザー操作をシミュレートできます。 クリック、入力、待機などを自動実行し、結果をアサーションで検証します。

import { within, userEvent, expect } from "@storybook/test";

// ログインフォームのインタラクションテスト
export const FilledForm: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);

    // メールアドレスを入力
    const emailInput = canvas.getByLabelText("メールアドレス");
    await userEvent.type(emailInput, "test@example.com");

    // パスワードを入力
    const passwordInput = canvas.getByLabelText("パスワード");
    await userEvent.type(passwordInput, "password123");

    // ログインボタンをクリック
    const submitButton = canvas.getByRole("button", {
      name: "ログイン",
    });
    await userEvent.click(submitButton);

    // 結果を検証
    await expect(
      canvas.getByText("ログイン成功")
    ).toBeInTheDocument();
  },
};

play functionsはStorybookのUIで実行状況がステップごとに表示されます。 また、test-runnerと組み合わせることで CIでも実行できます。

ストーリーの命名規則とベストプラクティス

ストーリーの命名を統一することで、チーム全体でコンポーネントカタログを効率的に活用できます。

// titleの階層化でサイドバーを整理
const meta: Meta<typeof Button> = {
  title: "Design System/Atoms/Button",
  //       ↑グループ    ↑カテゴリ ↑コンポーネント名
};

// ストーリー名の命名パターン
export const Default: Story = {};        // 基本状態
export const Small: Story = {};          // サイズバリエーション
export const Large: Story = {};
export const Disabled: Story = {};       // 無効状態
export const Loading: Story = {};        // ローディング状態
export const WithIcon: Story = {};       // アイコン付き
export const LongText: Story = {};       // エッジケース

推奨パターン

  • Defaultストーリーを必ず用意する
  • propsのバリエーションごとにストーリーを作成
  • エッジケース(長い文字列、空データ)も網羅
  • PascalCaseで命名する

避けるべきパターン

  • 1つのストーリーに複数状態を詰め込む
  • Story1、Story2のような意味のない名前
  • render関数内で大量のロジックを書く
  • ストーリーファイルの肥大化

まとめ

  • CSF3はdefault export(meta)と named export(Story)で構成される
  • argsでpropsを定義し、argTypesでControlsパネルの表示をカスタマイズ
  • decoratorsでテーマやレイアウトなどの外部コンテキストを提供できる
  • play functionsでユーザー操作のシミュレーションとアサーションが可能
  • ストーリー名は状態やバリエーションを明確にするPascalCaseで命名する