T-CREATOR

Storybook 情報設計の教科書:フォルダ/タイトル/ストーリー命名のベストプラクティス

Storybook 情報設計の教科書:フォルダ/タイトル/ストーリー命名のベストプラクティス

Storybook でコンポーネントカタログを作成する際、最も悩むのが「どう整理すればいいのか」という問題ではないでしょうか。フォルダ構成やタイトル設定、ストーリー名の付け方次第で、チーム全体の開発効率が大きく変わります。

本記事では、Storybook の情報設計におけるベストプラクティスを体系的に解説いたします。フォルダ構造からタイトル命名、ストーリーの整理方法まで、実務で使える知識を網羅的にお伝えしますので、ぜひ最後までご覧ください。

背景

Storybook は、UI コンポーネントを独立した環境で開発・管理できる強力なツールです。しかし、プロジェクトの規模が大きくなるにつれて、コンポーネントの数は数十から数百に膨れ上がっていきます。

このような状況で適切な情報設計がなされていないと、以下のような問題が発生してしまうでしょう。

  • 目的のコンポーネントを探すのに時間がかかる
  • 命名規則が統一されず、混乱を招く
  • 新しいメンバーが Storybook を理解しづらい

Storybook の情報設計は、単なる「整理整頓」ではありません。チーム全体の生産性を左右する、重要な設計判断なのです。

以下の図は、Storybook における情報設計の主要な構成要素を示しています。

mermaidflowchart TD
  fs["ファイルシステム"] -->|配置| story["ストーリーファイル"]
  story -->|CSF定義| meta["メタデータ<br/>(default export)"]
  meta -->|title属性| sidebar["サイドバー階層"]
  meta -->|component属性| info["コンポーネント情報"]
  story -->|named export| storyDef["各ストーリー"]
  storyDef -->|export名| display["表示名"]
  storyDef -->|name属性| custom["カスタム表示名"]
  sidebar -->|自動生成| url["URL/ID"]

上記の図から分かるように、ファイルシステム、メタデータ、ストーリー定義が相互に関連し合い、最終的な Storybook の構造を形成します。

課題

Storybook の情報設計において、開発者が直面する主な課題は以下の 3 つです。

フォルダ構成の複雑化

コンポーネントが増えると、フォルダ構造をどう設計するかが大きな課題となります。実装ファイルとストーリーファイルを別々に管理すると、メンテナンス性が低下してしまうでしょう。

一方で、すべてを同じフォルダに配置すると、ファイル数が多くなりすぎて見通しが悪くなります。

タイトル設定の不統一

Storybook のサイドバーに表示される階層構造は、title属性によって決まります。しかし、この設定方法を理解せずに進めると、以下のような問題が起こりがちです。

  • 同じようなコンポーネントが異なる階層に配置される
  • 命名規則がバラバラで、検索性が低い
  • 日本語と英語が混在し、統一感がない

ストーリー命名の曖昧さ

各コンポーネントには複数のストーリー(バリエーション)が存在します。これらをどう命名すべきか、明確な基準がないと混乱を招くでしょう。

例えば、ボタンコンポーネントに「Primary」「Secondary」というストーリー名を付けるのか、それとも「プライマリボタン」「セカンダリボタン」とするのか。こうした小さな判断の積み重ねが、全体の品質に影響します。

以下の図は、よくある課題のパターンを示しています。

mermaidflowchart LR
  issue1["フォルダ構成"] -->|課題| scatter["ファイルの散在"]
  issue2["タイトル設定"] -->|課題| inconsist["階層の不統一"]
  issue3["ストーリー命名"] -->|課題| ambiguous["意図不明な名前"]

  scatter -->|影響| maint["メンテナンス性<br/>低下"]
  inconsist -->|影響| search["検索性低下"]
  ambiguous -->|影響| understand["理解困難"]

これらの課題を放置すると、Storybook が「便利なツール」から「使いにくい負債」へと変わってしまいます。

解決策

Storybook の情報設計における課題を解決するため、公式ドキュメントで推奨されているベストプラクティスを 3 つの観点から解説いたします。

フォルダ構成のベストプラクティス

Storybook の公式ドキュメントでは、ストーリーファイルをコンポーネントファイルの隣に配置することを推奨しています。

具体的には、以下のようなファイル構成が理想的です。

typescriptcomponents/
├─ Button/
│  ├─ Button.tsx          // コンポーネント本体
│  ├─ Button.stories.tsx  // ストーリー定義
│  ├─ Button.test.tsx     // テストファイル
│  └─ index.ts            // エクスポート

このアプローチには、以下のようなメリットがあります。

#メリット説明
1関連ファイルの集約コンポーネントに関するすべてのファイルが一箇所にまとまる
2インポートパスの短縮相対パスが短くなり、リファクタリングが容易
3削除時の安全性コンポーネント削除時に関連ファイルも一緒に削除しやすい

ストーリーファイルの命名規則は、ComponentName.stories.tsxという形式を使います。この規則に従うことで、Storybook が自動的にファイルを認識してくれるでしょう。

以下は、より複雑なフォルダ構成の例です。

typescriptcomponents/
├─ atoms/
│  ├─ Button/
│  │  ├─ Button.tsx
│  │  └─ Button.stories.tsx
│  └─ Input/
│     ├─ Input.tsx
│     └─ Input.stories.tsx
├─ molecules/
│  └─ FormField/
│     ├─ FormField.tsx
│     └─ FormField.stories.tsx
└─ organisms/
   └─ LoginForm/
      ├─ LoginForm.tsx
      └─ LoginForm.stories.tsx

このような Atomic Design のパターンを採用する場合でも、各コンポーネントフォルダ内にストーリーファイルを配置する原則は変わりません。

タイトル設定のベストプラクティス

Storybook のサイドバー階層は、CSF ファイルのdefault exportにおけるtitle属性で制御します。公式ドキュメントでは、ファイルシステムのパスを反映した階層構造を推奨していますね。

以下のコードは、基本的なタイトル設定の例です。

typescriptimport type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';

まず、必要な型定義とコンポーネントをインポートします。MetaStoryObjは、TypeScript での型安全性を確保するために必要な型です。

typescript// メタデータの定義
const meta = {
  title: 'Components/Atoms/Button', // サイドバーの階層構造
  component: Button, // 対象コンポーネント
  parameters: {
    layout: 'centered',
  },
  tags: ['autodocs'],
} satisfies Meta<typeof Button>;

export default meta;

title属性では、スラッシュ(​/​)で区切ることで階層構造を表現できます。この例では、「Components」→「Atoms」→「Button」という 3 階層の構造になるでしょう。

タイトル命名における推奨ルールは以下の通りです。

#ルール
1UpperCamelCase を使用Components​/​Forms​/​LoginForm
2スラッシュで階層化Design System​/​Atoms​/​Button
3ファイルパスと一致させるcomponents​/​atoms​/​ButtonComponents​/​Atoms​/​Button
4一貫性のある命名すべて英語、またはすべて日本語

CSF 3(Component Story Format 3)では、title属性を省略すると、ファイルパスから自動的にタイトルが生成されます。

typescript// titleを省略した場合の例
const meta = {
  component: Button,
  // title属性なし → ファイルパスから自動生成される
} satisfies Meta<typeof Button>;

ファイルがcomponents​/​atoms​/​Button​/​Button.stories.tsxに配置されている場合、自動的に「Components/Atoms/Button」というタイトルが設定されます。この機能を活用すると、命名の手間を省けますね。

ただし、自動生成に頼る場合は、フォルダ構成そのものを慎重に設計する必要があります。以下の図は、タイトル設定とサイドバー表示の関係を示しています。

mermaidflowchart TD
  file["Button.stories.tsx"] -->|manual| titleAttr["title: 'Components/Atoms/Button'"]
  file -->|auto| autoTitle["ファイルパスから自動生成"]

  titleAttr --> sidebar["サイドバー階層"]
  autoTitle --> sidebar

  sidebar --> level1["Components"]
  level1 --> level2["Atoms"]
  level2 --> level3["Button"]

  sidebar -->|同時生成| urlId["URL ID:<br/>components-atoms-button"]

タイトル設定は、サイドバーの表示だけでなく、Storybook の URL 構造にも影響を与えます。適切な階層設計が、検索性とメンテナンス性の向上につながるのです。

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

各コンポーネントには、複数のバリエーションを表現するストーリーを定義します。Storybook では、named export がストーリーとして認識される仕組みです。

以下のコードで、基本的なストーリー定義を見ていきましょう。

typescripttype Story = StoryObj<typeof meta>;

まず、Story 型を定義します。これにより、各ストーリーに型安全性が提供されるでしょう。

typescript// プライマリボタンのストーリー
export const Primary: Story = {
  args: {
    variant: 'primary',
    children: 'Button',
  },
};

ストーリーの export 名は、UpperCamelCase で記述することが推奨されています。この例ではPrimaryという名前を使っていますね。

typescript// セカンダリボタンのストーリー
export const Secondary: Story = {
  args: {
    variant: 'secondary',
    children: 'Button',
  },
};

複数のストーリーを定義することで、コンポーネントの様々な状態を表現できます。

Storybook は、export 名を自動的に「読みやすい形式」に変換してくれます。具体的には、Lodash のstartCase関数を使った変換が行われるのです。

#Export 名表示名
1Primary"Primary"
2WithLongText"With Long Text"
3loadingState"Loading State"
4someNAME"Some NAME"

この自動変換により、キャメルケースで書いた export 名が、スペース区切りの読みやすい表示名になります。

さらに、name属性を使うことで、表示名をカスタマイズすることも可能です。

typescript// カスタム表示名を設定した例
export const Primary: Story = {
  name: 'プライマリボタン', // カスタム表示名
  args: {
    variant: 'primary',
    children: 'ボタン',
  },
};

name属性を使えば、日本語での表示名も設定できますね。プロジェクトの方針に応じて、英語と日本語を使い分けましょう。

以下は、実践的なストーリー命名の例です。

typescript// デフォルト状態
export const Default: Story = {
  args: {
    children: 'Button',
  },
};

// 無効化状態
export const Disabled: Story = {
  args: {
    disabled: true,
    children: 'Disabled Button',
  },
};

// 長いテキスト
export const WithLongText: Story = {
  name: 'With Very Long Button Label',
  args: {
    children:
      'This is a button with an extremely long label that might cause layout issues',
  },
};

// アイコン付き
export const WithIcon: Story = {
  args: {
    children: 'Button',
    icon: 'search',
  },
};

ストーリー命名における推奨ルールをまとめると、以下のようになります。

#ルール理由
1UpperCamelCase で記述自動変換が正しく機能する
2状態や用途を明確に意図が伝わりやすい(例:DisabledLoading
3プレフィックスで分類関連ストーリーをグループ化(例:With*系)
4必要に応じてname属性を使用より詳細な説明が必要な場合

以下の図は、ストーリー定義から表示までの流れを示しています。

mermaidflowchart LR
  export["named export"] -->|例: WithLongText| convert["startCase変換"]
  export -->|name属性あり| customName["カスタム名優先"]

  convert --> display1["With Long Text"]
  customName --> display2["指定した表示名"]

  display1 --> ui["Storybook UI"]
  display2 --> ui

  ui --> user["開発者が確認"]

適切なストーリー命名により、チームメンバーは目的のバリエーションを素早く見つけられるようになります。

具体例

ここまで解説したベストプラクティスを、実際のプロジェクトでどう適用するか、具体例を通して見ていきましょう。

ケーススタディ:デザインシステムの構築

企業のデザインシステムを構築する場合、数十から数百のコンポーネントを管理する必要があります。ここでは、中規模プロジェクトを想定した実装例を紹介いたしますね。

まず、プロジェクト全体のフォルダ構成を設計します。

plaintextsrc/
├─ components/
│  ├─ design-system/
│  │  ├─ primitives/        // 最も基本的な要素
│  │  │  ├─ Button/
│  │  │  ├─ Input/
│  │  │  └─ Text/
│  │  ├─ patterns/          // 複合的なパターン
│  │  │  ├─ FormField/
│  │  │  ├─ Card/
│  │  │  └─ Modal/
│  │  └─ layouts/           // レイアウト用コンポーネント
│  │     ├─ Container/
│  │     ├─ Grid/
│  │     └─ Stack/
│  └─ features/             // 機能別コンポーネント
│     ├─ auth/
│     └─ dashboard/

このフォルダ構成は、コンポーネントの役割と抽象度に基づいて階層化されています。それぞれの階層には明確な責任範囲があるでしょう。

次に、具体的なコンポーネントの実装例を見ていきます。まずは、Button コンポーネントです。

typescript// src/components/design-system/primitives/Button/Button.tsx
import { ReactNode, ButtonHTMLAttributes } from 'react';
import styles from './Button.module.css';

必要な依存関係をインポートします。React の型定義と CSS モジュールを使用していますね。

typescript// Buttonコンポーネントのプロパティ型定義
export interface ButtonProps
  extends ButtonHTMLAttributes<HTMLButtonElement> {
  /** ボタンのバリアント */
  variant?: 'primary' | 'secondary' | 'danger';
  /** ボタンのサイズ */
  size?: 'small' | 'medium' | 'large';
  /** 全幅表示するか */
  fullWidth?: boolean;
  /** 子要素(ボタンのラベルなど) */
  children: ReactNode;
}

型定義では、各プロパティに JSDoc コメントを付けることで、Storybook のドキュメント自動生成に活用できます。

typescript// Buttonコンポーネントの実装
export const Button = ({
  variant = 'primary',
  size = 'medium',
  fullWidth = false,
  children,
  className,
  ...props
}: ButtonProps) => {
  // クラス名の組み立て
  const classNames = [
    styles.button,
    styles[variant],
    styles[size],
    fullWidth && styles.fullWidth,
    className,
  ]
    .filter(Boolean)
    .join(' ');

  return (
    <button className={classNames} {...props}>
      {children}
    </button>
  );
};

コンポーネントの実装では、プロパティに応じて適切なクラス名を組み立てています。

続いて、この Button コンポーネントのストーリーファイルを作成しましょう。

typescript// src/components/design-system/primitives/Button/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';

型定義とコンポーネントをインポートします。

typescript// メタデータの定義
const meta = {
  title: 'Design System/Primitives/Button',
  component: Button,
  parameters: {
    layout: 'centered',
    docs: {
      description: {
        component:
          'プライマリアクションやフォーム送信に使用する基本的なボタンコンポーネントです。',
      },
    },
  },
  tags: ['autodocs'],
  argTypes: {
    variant: {
      control: 'select',
      options: ['primary', 'secondary', 'danger'],
      description: 'ボタンの視覚的なバリエーション',
    },
    size: {
      control: 'select',
      options: ['small', 'medium', 'large'],
      description: 'ボタンのサイズ',
    },
  },
} satisfies Meta<typeof Button>;

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

メタデータでは、titleで階層構造を定義し、argTypesで各プロパティの説明を追加しています。これにより、Storybook のドキュメントが充実するでしょう。

typescript// デフォルトストーリー
export const Default: Story = {
  args: {
    children: 'Button',
  },
};

最もシンプルな状態をDefaultストーリーとして定義します。

typescript// バリアント別のストーリー
export const Primary: Story = {
  args: {
    variant: 'primary',
    children: 'Primary Button',
  },
};

export const Secondary: Story = {
  args: {
    variant: 'secondary',
    children: 'Secondary Button',
  },
};

export const Danger: Story = {
  args: {
    variant: 'danger',
    children: 'Delete',
  },
};

各バリアントごとにストーリーを作成することで、デザインの一貫性を確認できます。

typescript// サイズバリエーション
export const SmallSize: Story = {
  name: 'Small',
  args: {
    size: 'small',
    children: 'Small Button',
  },
};

export const LargeSize: Story = {
  name: 'Large',
  args: {
    size: 'large',
    children: 'Large Button',
  },
};

サイズのバリエーションも、それぞれストーリーとして定義します。name属性でシンプルな表示名を設定していますね。

typescript// 状態のバリエーション
export const Disabled: Story = {
  args: {
    disabled: true,
    children: 'Disabled Button',
  },
};

export const FullWidth: Story = {
  name: 'Full Width',
  args: {
    fullWidth: true,
    children: 'Full Width Button',
  },
};

無効化状態や全幅表示など、UI の状態もストーリーで表現します。

typescript// 実践的なユースケース
export const WithLongText: Story = {
  name: 'With Long Label',
  args: {
    children:
      'This is a button with a very long label that might wrap',
  },
};

export const SubmitForm: Story = {
  name: 'Form Submit (Primary Large)',
  args: {
    variant: 'primary',
    size: 'large',
    type: 'submit',
    children: 'Submit Form',
  },
};

実際の利用シーンを想定したストーリーを用意することで、実装者の理解が深まります。

このストーリーファイルにより、Storybook では以下のような階層構造が生成されるでしょう。

mermaidflowchart TD
  root["Storybook サイドバー"] --> ds["Design System"]
  ds --> prim["Primitives"]
  prim --> btn["Button"]

  btn --> story1["Default"]
  btn --> story2["Primary"]
  btn --> story3["Secondary"]
  btn --> story4["Danger"]
  btn --> story5["Small"]
  btn --> story6["Large"]
  btn --> story7["Disabled"]
  btn --> story8["Full Width"]
  btn --> story9["With Long Label"]
  btn --> story10["Form Submit (Primary Large)"]

この図が示すように、適切な階層設計により、開発者は目的のストーリーを素早く見つけられます。

複雑なコンポーネントの例

次に、より複雑なコンポーネントの例として、FormField コンポーネントを見ていきましょう。

typescript// src/components/design-system/patterns/FormField/FormField.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { FormField } from './FormField';

まず、必要な型とコンポーネントをインポートします。

typescriptconst meta = {
  title: 'Design System/Patterns/FormField',
  component: FormField,
  parameters: {
    layout: 'padded',
  },
  tags: ['autodocs'],
} satisfies Meta<typeof FormField>;

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

FormField は「Patterns」カテゴリに配置されており、複合的なコンポーネントであることが階層から分かりますね。

typescript// 基本的なストーリー
export const Default: Story = {
  args: {
    label: 'Email',
    type: 'email',
    placeholder: 'Enter your email',
  },
};

// バリデーションエラー
export const WithError: Story = {
  name: 'Validation Error',
  args: {
    label: 'Email',
    type: 'email',
    value: 'invalid-email',
    error: 'Please enter a valid email address',
  },
};

// ヘルプテキスト
export const WithHelperText: Story = {
  name: 'With Helper Text',
  args: {
    label: 'Password',
    type: 'password',
    helperText: 'Must be at least 8 characters',
  },
};

// 必須フィールド
export const Required: Story = {
  args: {
    label: 'Username',
    required: true,
    placeholder: 'Enter username',
  },
};

FormField のような複雑なコンポーネントでは、各プロパティの組み合わせをストーリーとして網羅することが重要です。

以下の図は、FormField コンポーネントの状態遷移を示しています。

mermaidstateDiagram-v2
  state "Default" as DFLT
  state "Error" as ERR

  [*] --> DFLT: 初期表示
  DFLT --> Focused: フォーカス
  Focused --> DFLT: フォーカス解除
  Focused --> Validating: 入力完了
  Validating --> Valid: バリデーション成功
  Validating --> ERR: バリデーション失敗
  ERR --> Focused: 再入力
  Valid --> [*]: 送信

このような状態遷移を意識してストーリーを作成すると、すべての UI パターンを網羅できるでしょう。

命名規則の統一例

実際のプロジェクトでは、チーム全体で命名規則を統一することが重要です。以下は、チームで共有できる命名規則の例になります。

#対象命名ルール
1フォルダ名PascalCaseButton​/​FormField​/​
2ストーリーファイル名{ComponentName}.stories.tsxButton.stories.tsx
3title 属性Category​/​Subcategory​/​ComponentDesign System​/​Primitives​/​Button
4ストーリー export 名UpperCamelCasePrimaryWithError
5カスタム表示名自然な英語With Long LabelValidation Error

このような明文化されたルールをプロジェクトの README やコーディングガイドラインに記載しておくと、新メンバーのオンボーディングがスムーズになりますね。

.storybook ディレクトリの設定例

最後に、プロジェクト全体の Storybook 設定を見ていきましょう。

typescript// .storybook/main.ts
import type { StorybookConfig } from '@storybook/react-vite';

const config: StorybookConfig = {
  // ストーリーファイルの検索パターン
  stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],

  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
  ],

  framework: {
    name: '@storybook/react-vite',
    options: {},
  },

  docs: {
    autodocs: 'tag', // 自動ドキュメント生成
  },
};

export default config;

stories配置パターンを適切に設定することで、プロジェクト内のすべてのストーリーファイルを自動的に読み込めます。

これらの具体例を参考に、プロジェクトの規模や要件に応じた情報設計を行ってください。

まとめ

Storybook の情報設計は、単なる整理整頓ではなく、チーム全体の開発効率を左右する重要な設計判断です。本記事で解説したベストプラクティスをまとめると、以下のようになります。

フォルダ構成では、ストーリーファイルをコンポーネントファイルの隣に配置することで、関連ファイルの一元管理と保守性の向上が実現できます。

タイトル設定では、ファイルシステムのパスを反映した階層構造を採用し、UpperCamelCase とスラッシュ区切りで一貫性のある命名を行います。CSF 3 の自動生成機能も活用できるでしょう。

ストーリー命名では、UpperCamelCase の named export を基本とし、状態や用途を明確に表現する名前を付けます。必要に応じてname属性でカスタマイズすることも有効です。

これらのベストプラクティスを実践することで、チームメンバーは目的のコンポーネントを素早く見つけられ、新メンバーのオンボーディングもスムーズになります。さらに、一貫性のある情報設計は、デザインシステムの品質向上にも貢献するでしょう。

プロジェクトの成長に合わせて、定期的に情報設計を見直し、改善を続けることをお勧めいたします。適切な情報設計が、Storybook を真に価値あるツールへと変えてくれるはずです。

関連リンク