デザインシステム コンポーネント設計ガイドライン
本ドキュメントは、チーム全体で一貫性のある高品質なUIコンポーネントを設計・実装するための基準を定めたものだ。Storybookを用いたコンポーネント駆動開発を前提とし、コンポーネントの分割、ディレクトリ構造、命名規則、依存関係のルールについて整理している。
1. コンポーネントの分割・分類基準(ディレクトリ設計)
コンポーネントは、「汎用性(どこでも使えるか)」と「ドメイン依存(特定のデータモデルに依存するか)」 を軸に分割し、依存関係を一方向(上から下)に保つ。
1-1. 推奨されるディレクトリ構成の概念 (Feature-Sliced Designのマッピング)
デザインシステムの概念である「汎用性」と「ドメイン依存」を、プロジェクトのディレクトリ構成(Bulletproof Reactベース)にマッピングして管理する。
src/
├── components/ # UI Kit: 汎用コンポーネント (Button, Input, Avatar など)
│ # ※ アプリケーション全体で使い回せる「ui」層に相当する
├── layouts/ # Layouts: アプリ全体で共有する専用ウィジェット (GlobalHeader など)
└── features/
└── [feature]/
└── components/ # Domain/Widgets: 特定の機能に特化したコンポーネント群
# ※ 物理的には同階層でも、ドメイン部品(domain)と組み合わせブロック(widgets)を概念的に意識して設計する
- 依存のルール: 上位層(
widgets>domain>ui)は下位層を利用できるが、下位層(ui)は上位層に依存 してはならない。 - 構築戦略: 複雑なUIはゼロから自作せず、Radix UI等のHeadless UIを土台とし自社のスタイルを被せる手法を推奨する。
1-2. 汎用コンポーネント (UI Kit) src/components/
- 特定のビジネスロジックやデータモデルに依存せず、別プロジェクトでも転用可能なレベルの抽象度を持つ。
- プロパティごとに個別の値を受け取る設計にする(例:
Userオブジェクトを丸ごと渡すのではなく、name,imageUrlとして渡す)。 - アプリケーション全体で共有するため、
src/components/直下に配置する。
- 構成の例: コンポーネントの「役割」等での階層化は避け、フラット(平坦) に直列で配置する。
src/components/
├── Button/ # コンポーネント単位でディレクトリを分けるか、もしくは Button.tsx のように 配置
├── IconButton/ # ※Buttonの内部に作らず、同じ階層にフラットに配置する
├── TextField/
├── Dialog/
├── Heading/
├── Alert/
└── Tabs/
1-3. ドメインコンポーネント (Domain) src/features/[feature]/components/
- バックエンドのデータモデルや特定の機能に紐づくコンポーネント。
- APIスキーマやデータモデルの構造(ObjectやArray)をそのままプロパティとして受け取ってよい。
features/[feature]/components/内において、画面全体を構成するブロック(Widgets)を組み立てるための「意味を持った部品」として機能する。- 共通化の罠 (Rule of Three): 見た目が似ているからといってすぐに共通化しない。同じパターンが3回登場したら共通化を検討する。
- 構成の例: アプリケーションの機能(Feature)ごとに分類して配置する。これにより、関連するロジック(hooks等)と同じ凝集度で管理できる。
features/
├── user/ # user関連の機能ドメイン
│ └── components/
│ └── UserCard/ # UserList, UserAvatar などユーザー関連
├── product/ # product関連の機能ドメイン
│ └── components/
│ └── ProductListItem/
└── auth/
└── components/
└── LoginForm/
1-4. ウィジェット (Widgets) src/features/[feature]/components/ もしくは src/layouts/
- 汎用コンポーネントとドメインコンポーネントを組み合わせ、独立して機能する画面の大きなブロック(セクション)を構成する。
- ページコンポーネントは、このWidgetsを並べるだけで完成する状態が理想だ。
features/[feature]/components/内に置かれる場合、ドメイン(Domain)コンポーネントとは物理的には同階層になる。これらは命名規則(例:*Section,*Block)や設計者の意識によって「これは独立した大きなブロックである」と概念的に区別する。
-
構成の例: 画面上の「大きな機能ブロック」ごとに分類する。特定の機能に依存する場合は
features/に配置する。アプリケーション全体で共有するレイアウトの枠組み(ヘッダー等)は、汎用UI(src/components/)と区別するためsrc/layouts/などに配置する。src/layouts/
├── GlobalHeader/ # ロゴ、ナビゲーション、ユーザーメニューを含む全体のブロック
└── GlobalFooter/ # アプリケーション共通のフッター
features/
└── report/
└── components/
└── ReportDashboardSidebar/ # 特定の機能における複雑なブロック(概念的なWidgets層として実装)
2. 命名とプロパティ (Props) の設計
2-1. コンポーネントの命名
- 業界標準に合わせる:
DialogやPopoverなど、既存の語彙に揃える。 - 状態・バリアントを名前に含めない:
LoadingButtonのような命名は避け、Button+isLoadingプロパティで表現する。 - レイアウトバリエーションの接尾辞: 形状で名前のルールを統一する(
*Card: リッチな縦型、*Cell: 一覧用の横幅いっぱい、*Item: 最小の行表示)。
2-2. プロパティの型と直交性
- 直交性: プロパティ同士は独立させる。例:
style="ghost"とstyle="danger"を混ぜず意味と強調度に分ける。 - 必須と任意: 必須Propsを最小限にし、よく使われる値をデフォルト(例:
size="medium")に設定する。 - Boolean型(真偽値):
isDisabled,hasIcon,showDividerなどポジティブな命名にする。もし排他的に3つ以上増えそうなら早めに Enum型(列挙型)position="left"|"right"|"none"へ昇格させる。
2-3. バリアント爆発を防ぐ Slot (Element) パターン
hasLeftIconのよ うなBooleanを量産するのではなく、**「差し込み口(Slot)」**を用意しReactNode等を受け取る設計にする。- プロパティが10個以上に膨らんできた場合は、設定値(Configuration)ではなく、スロット(Composition)に切り替えるサイン。
3. コンポーネントの責任範囲とレイアウト
コンポーネントは「自分の中身だけに責任を持ち、どこに置くかは親要素の責務」とするカプセル化を徹底する。
3-1. 余白 (Margin / Padding)
- 原則: コンポーネント自体に外側の余白(
margin)を持たせない。 - 内側の余白(
padding)はコンポーネントの責任。隣接要素との間隔は、配置する親(Flexbox要素などのgap)が制御する。
3-2. 幅の振る舞い (Width) と オーバーフロー
- コンポーネントのデフォルト幅を明確にする。極端な崩れを防ぐため
min-widthやmax-widthを設定する。 - テキストのオーバーフロー:
text-overflow: ellipsis(省略)、line-clamp(行数制限で折り返す)などを仕様として明記する。
3-3. アセット(アイコン等)の管理
- アイコンは画像(PNG等)でなく、SVGまたはアイコンライブラリ(Reactコンポーネント) として実装する。
- 色の自動追従: ダークモード等に対応するため、SVGの塗りは
#1A1A1Aなどの固定値にせず、currentColorを使用し親のテキスト色に自動で一致させる。
4. UIスタック(状態の網羅)と運用
4-1. 5つのUIスタック
StorybookのStories作成時には、データが理想的に入ってくる状態だけでなく、現実的な各状態を網羅する。
- Ideal State: 全データが揃っている理想状態。
- Empty State: データ0件時の案内やプレースホルダー。
- Loading State: スケルトンスクリーンやスピナー表示。
- Partial State: データが部分欠損している状態のフォールバック(例: 画像URLがない場合はイニシャル表示)。
- Error State: データ取得失敗時のフォールバック。
4-2. 変更の安全性とライフサイクル
- 追加は安全、変更は危険: 新しいバリアントの追加は安全(後方互換性あり)だが、既存のProps名変更や削除は破壊的変更となり修正コストが高い。
- 廃止設計: 古いコンポーネントを消す際は、
[Deprecated]や@deprecatedで警告を出して移行を促し、使われなくなった後に削除する。