ストーリーの書き方
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で命名する