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';
まず、必要な型定義とコンポーネントをインポートします。Meta
とStoryObj
は、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 階層の構造になるでしょう。
タイトル命名における推奨ルールは以下の通りです。
# | ルール | 例 |
---|---|---|
1 | UpperCamelCase を使用 | Components/Forms/LoginForm |
2 | スラッシュで階層化 | Design System/Atoms/Button |
3 | ファイルパスと一致させる | components/atoms/Button → Components/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 名 | 表示名 |
---|---|---|
1 | Primary | "Primary" |
2 | WithLongText | "With Long Text" |
3 | loadingState | "Loading State" |
4 | someNAME | "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',
},
};
ストーリー命名における推奨ルールをまとめると、以下のようになります。
# | ルール | 理由 |
---|---|---|
1 | UpperCamelCase で記述 | 自動変換が正しく機能する |
2 | 状態や用途を明確に | 意図が伝わりやすい(例:Disabled 、Loading ) |
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 | フォルダ名 | PascalCase | Button/ 、FormField/ |
2 | ストーリーファイル名 | {ComponentName}.stories.tsx | Button.stories.tsx |
3 | title 属性 | Category/Subcategory/Component | Design System/Primitives/Button |
4 | ストーリー export 名 | UpperCamelCase | Primary 、WithError |
5 | カスタム表示名 | 自然な英語 | With Long Label 、Validation 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 を真に価値あるツールへと変えてくれるはずです。
関連リンク
- article
Storybook 情報設計の教科書:フォルダ/タイトル/ストーリー命名のベストプラクティス
- article
Storybook Args/ArgTypes 速見表:Controls/Docs/Autodocs を一気に整える
- article
Storybook を Monorepo に導入:Yarn Workspaces/Turborepo の最短レシピ
- article
Storybook Builder 徹底比較:Vite vs Webpack vs Rspack の速度と互換性
- article
Storybook が真っ白!起動しない/ビルド失敗の原因と 15 の対処チェック
- article
Storybook アーキテクチャ完全図解:Preview/Manager/Builder が噛み合う瞬間
- article
Vue.js コンポーネント API 設計:props/emit/slot を最小 API でまとめる
- article
GitHub Copilot 前提のコーディング設計:コメント駆動 → テスト → 実装の最短ループ
- article
Tailwind CSS マルチブランド設計:CSS 変数と data-theme で横断対応
- article
Svelte フォーム体験設計:Optimistic UI/エラー復旧/再送戦略の型
- article
GitHub Actions でゼロダウンタイムリリース:canary/blue-green をパイプライン実装
- article
Git エイリアス 50 連発:長コマンドを一行にする仕事術まとめ
- blog
iPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
- blog
Googleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
- blog
【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
- blog
Googleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
- blog
Pixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
- blog
フロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来