PackagesUIArchitecture
コンポーネントアーキテクチャ
3層構造 + Vendorsの全体像、共通原則、レイヤー間の依存ルール。
3層構造 + Vendors
コンポーネントは Primitives・Elements・Composites の3層で構成する。各層は明確に異なる責務を持ち、依存は常に上位層から下位層への一方向のみ許可する。
これに加えて、外部サービスのブランドに紐づくコンポーネントを格納する Vendors レイヤーが存在する。Vendorsは3層構造とは独立した軸に位置し、Primitivesに依存しない。
依存の方向: Composites → Elements → Primitives
(上位層が下位層に依存する。逆方向の依存は禁止)
Vendors は3層から独立。Primitives・Elements・Composites に依存しない。┌─────────────────────────────────────────────┐
│ Composites(複合コンポーネント) │ ← 外部エクスポート
│ 複数の Elements を組み合わせた完結したUI単位 │
├─────────────────────────────────────────────┤
│ Elements(単機能コンポーネント) │ ← 外部エクスポート
│ Primitives を組み合わせた用途特化のコンポーネント│
├─────────────────────────────────────────────┤
│ Primitives(基盤コンポーネント) │ ← 内部のみ
│ スタイル付きの最小構成単位 │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ Vendors(外部ブランドコンポーネント) │ ← 外部エクスポート
│ 外部サービスのブランドに準拠したUI │
└─────────────────────────────────────────────┘| 層 | 役割 | 判断基準 | エクスポート |
|---|---|---|---|
| Primitives | どう見えるか(what)を定義する | 色・サイズ・角丸・hover色などスタイルのバリエーション | 内部のみ |
| Elements | いつ・なぜそう見えるか(when / why)を決める | 状態に応じたvariant選択、Primitivesの組み合わせ | 外部公開 |
| Composites | 複数のElementsを統合したUI単位を提供する | 子要素間の連携、開閉・フォーカス管理 | 外部公開 |
| Vendors | 外部ブランドに準拠したUIを提供する | 外部サービスのブランドガイドラインに縛られるか | 外部公開 |
共通原則
すべてのレイヤーに適用する設計原則を定義する。
Props設計
React.ComponentProps<"element">を拡張し、variant・独自propsを追加する- Props名は対象が明確な名前にする。
typeやvalueのような汎用名よりも、variantやiconNameのように意味が限定される名前を選ぶ - booleanのpropsは
is/hasで始める(例:isLoading,hasIcon) - コールバックは
onで始める(例:onClose,onChange) - 選択肢がある場合、
stringではなくユニオン型で限定する
// ❌ stringで受けると何でも渡せてしまう
interface BadProps {
size: string;
}
// ✅ ユニオン型で選択肢を限定する
interface GoodProps {
size: 'sm' | 'md' | 'lg';
}スタイリング
- CVA(class-variance-authority)でvariantを定義する
cn()でクラスを合成する- Tailwind CSSのユーティリティクラスのみを使用する。
style属性やarbitrary values(text-[13px])は禁止する - 色・スペーシング・角丸等はすべてデザイントークン経由で指定する
- 外部エクスポートするコンポーネント(Elements / Composites)は
classNameを受け付けない。利用者側のスタイル上書きを防ぎ、デザインの一貫性を保証する - Primitives層では
classNameを受け取りcn()で内部クラスとマージする。これはデザインシステム内部での合成に限定される
スペーシング
コンポーネント内部の余白と外部の余白は、責務を持つ層が異なる。
| 種類 | 責務 | 例 |
|---|---|---|
| 内部余白(padding, gap) | Primitives | ボタン内の px-4 py-2、アイコンとテキストの gap-2 |
| 外部余白(margin) | コンポーネントは持たない | レイアウトコンポーネント(Stack, Grid等)の gap で制御する |
コンポーネントは自身の外側の余白を持たない。配置先のコンテキストによって適切な余白は変わるため、外部余白はレイアウトコンポーネントの gap で制御する。
// ❌ Primitiveが外部余白を持つ
const Button = ({ className, ...props }) => (
<button className={cn('mb-4 px-4 py-2', className)} {...props} />
);
// ✅ 内部余白のみ。外部余白はレイアウトコンポーネントが担う
const Button = (props) => <button className="px-4 py-2" {...props} />;
// ✅ 利用者側ではレイアウトコンポーネントで間隔を制御する
<Stack gap="md">
<Button />
<Button />
</Stack>;フィードバックの責務
ユーザー操作に対するフィードバックは、種類によって担う層が異なる。インタラクションデザインで定義された各フィードバックの実装責務を以下に示す。
| フィードバック | 担う層 | 実装方法 |
|---|---|---|
| hover時の色変化 | Primitives | CSS擬似クラス hover: |
| active / pressed時の色変化 | Primitives | CSS擬似クラス active: |
| focus リング | Primitives | CSS擬似クラス focus-visible: |
| disabled の見た目(透明度等) | Primitives | CSS擬似クラス disabled: / aria-disabled: |
| loading → Spinner表示 | Elements | 条件分岐で Spinner を表示、disabled を渡す |
| error → 赤枠 + メッセージ | Elements | aria-invalid を渡し、エラーメッセージを表示する |
| 確認ダイアログ(破壊的操作) | Composites | Dialog で操作前に確認を挟む |
| Toast通知(成功・失敗) | Composites | 操作完了後に Toast で結果を通知する |
Primitivesは 見た目の変化 を定義し、Elements は いつその変化を起こすか を制御し、Composites は 操作の前後に挟むフロー を管理する。
data属性
すべてのコンポーネントに以下のdata属性を付与する。テスト・CSS セレクタ・デバッグに使用する。
| 属性 | 用途 | 例 |
|---|---|---|
data-slot | コンポーネントの識別 | data-slot="button" |
data-variant | 現在のvariant | data-variant="default" |
data-size | 現在のsize | data-size="md" |
アクセシビリティ
- WAI-ARIAパターンに準拠する。対応するARIAロールが存在する場合は必ず実装する
- キーボード操作はコンポーネント内で完結させる
- フォーカスリングは
focus-visibleで制御し、削除しない - 装飾的な要素(アイコン等)には
aria-hidden="true"を付与する - 色だけで情報を伝えない。アイコン・テキスト等を併用する
命名規約
- コンポーネント名はPascalCase(例:
ActionButton) - ファイル名はkebab-case(例:
action-button.tsx) - Props型はコンポーネント名 +
Props(例:ActionButtonProps) - CVA定義は
コンポーネント名(小文字)Variants(例:buttonVariants) - named exportのみ。default exportは禁止する
エクスポート規約
- Primitivesは
@bi-shop-it/uiから外部エクスポートしない - Elements / Composites / Vendors は
@bi-shop-it/ui/<component-name>でフラットにエクスポートする - 型(Props型、ユニオン型等)は利用者が必要とする場合にのみエクスポートする
レイヤー間の依存ルール
許可する依存
Composites → Elements → Primitives
→ ユーティリティ(cn, hooks)
Vendors → ユーティリティ(cn)のみ- Primitives は他のどの層にも依存しない(ユーティリティを除く)
- Elements は Primitives とユーティリティにのみ依存する
- Composites は Elements とユーティリティにのみ依存する
- Vendors はユーティリティにのみ依存する。3層構造のどの層にも依存しない
禁止する依存
| 禁止パターン | 理由 |
|---|---|
| Primitives → Elements | 上位層への逆依存は循環を生む |
| Primitives → Composites | 同上 |
| Elements → Composites | 同上 |
| Elements → Elements | 横方向の依存はレイヤーの境界を曖昧にする |
| Composites → Composites | 同上 |
| Composites → Primitives | レイヤーの飛び越えは Elements の存在意義を失わせる |
| Vendors → Primitives | Vendorsは外部ブランドのスタイルを使うため、Primitivesのvariantに依存しない |
| Vendors → Elements | Vendorsは3層構造から独立したレイヤーである |
| Vendors → Composites | 同上 |
例外
Iconは他のElementsから参照することを許可する。Iconはユーティリティに近い性質を持ち、他のElementsの内部で装飾として使用されるケースが多いためである