コンポーネントファースト戦略
問題
プロジェクトが Tailwind CSS のようなユーティリティファースト CSS フレームワークとコンポーネントフレームワーク(React、Vue、Svelte など)を併用している場合、開発者や AI エージェントは従来の CSS パターンに回帰しがちです。コンポーネント内でユーティリティクラスを組み合わせる代わりに、.profile-card、.btn-primary、.sidebar-nav のようなカスタム CSS クラス名を作成し、別のスタイルシートや CSS Modules を使ってしまいます。
これによりコードベースが断片化します:
- Tailwind ユーティリティをインラインで使うコンポーネントもあれば
- BEM 命名や CSS Modules でカスタム CSS クラスを導入するコンポーネントもあり
- 同じファイル内で両方のアプローチを混在させるコンポーネントもある
この一貫性のなさがプロジェクトの保守を難しくします。UI のある部分がユーティリティで、カスタム CSS で、あるいはその両方でスタイリングされているのか、確信が持てなくなります。
AI エージェントにとって、これは特によくある失敗パターンです。「プロフィールカードを作って」というタスクを与えると、エージェントはトレーニングデータで最も多く見られるパターンである CSS Module 付きの .profile-card クラスを生成しがちです。Tailwind を専用で使っているプロジェクトであっても、AI が生成したコードがプロジェクトの規約から逸脱するカスタム CSS クラスを持ち込みます。時間の経過とともに、コードベースは相反するスタイリングアプローチのパッチワークになっていきます。
解決方法
コンポーネントファースト戦略: プロジェクトがコンポーネントベースのフレームワークとユーティリティ CSS フレームワークを使っている場合、UI は常にユーティリティクラスを持つコンポーネントとして表現します。UI レベルの CSS クラス名を別のスタイルシートで作成してはいけません。
- カードが必要? ユーティリティクラスを使った
<Card>コンポーネントを作成する - ボタンのバリアントが必要?
<Button variant="primary">コンポーネントを作成する - レイアウトパターンが必要?
<PageLayout>コンポーネントを作成する
コンポーネント自体が抽象化です。.card や .btn-primary のような CSS クラス名は不要です。コンポーネントがカプセル化を担い、ユーティリティクラスがスタイリングを担います。
コード例
アンチパターン: Tailwind プロジェクトでのカスタム CSS クラス
コンポーネントベースの Tailwind プロジェクトでやってはいけない例です。カスタム CSS クラス名と別のスタイルシートを作成し、ユーティリティフレームワークを完全に迂回しています:
// ProfileCard.module.css
// .profileCard { display: flex; gap: 1rem; padding: 1.5rem; ... }
// .avatar { width: 64px; height: 64px; border-radius: 50%; ... }
// .name { font-size: 1.25rem; font-weight: 600; ... }
// .role { color: #6b7280; font-size: 0.875rem; ... }
import styles from './ProfileCard.module.css';
function ProfileCard({ name, role, avatar }) {
return (
<div className={styles.profileCard}>
<img className={styles.avatar} src={avatar} alt="" />
<div>
<h3 className={styles.name}>{name}</h3>
<p className={styles.role}>{role}</p>
</div>
</div>
);
}
このアンチパターンの CSS は従来のコンポーネント CSS のように見えます。カスタムクラス名、別ファイル、BEM 風の命名です:
見た目は問題ありませんが、命名の判断、別の CSS ファイル、そしてプロジェクトの他の Tailwind ベースの部分と競合するスタイリングアプローチを持ち込んでしまいます。
推奨: コンポーネントファースト + ユーティリティクラス
同じ結果を、コンポーネント内に直接ユーティリティクラスを組み合わせて実現します:
function ProfileCard({ name, role, avatar }) {
return (
<div className="flex gap-4 p-6 bg-white rounded-lg shadow-md">
<img
className="w-16 h-16 rounded-full object-cover"
src={avatar}
alt=""
/>
<div className="flex flex-col justify-center">
<h3 className="text-xl font-semibold text-slate-800">{name}</h3>
<p className="text-sm text-gray-500 mt-1">{role}</p>
</div>
</div>
);
}
CSS ファイルは不要です。考えるべきクラス名もありません。コンポーネントがビジュアルデザインをカプセル化します。カードの見た目を変更したいときは、コンポーネントという1つのファイルを編集するだけです。
Props によるコンポーネントバリアント
CSS の修飾子クラス(.btn--primary、.btn--secondary)を作る代わりに、コンポーネントの props でバリアントを制御します:
function Button({ variant = 'primary', children, ...props }) {
const styles = {
primary: 'bg-blue-500 hover:bg-blue-700 text-white',
secondary: 'bg-gray-200 hover:bg-gray-300 text-gray-800',
outline:
'bg-transparent hover:bg-blue-50 text-blue-600 border border-blue-500',
};
return (
<button
className={`${styles[variant]} font-semibold py-2 px-4 rounded`}
{...props}
>
{children}
</button>
);
}
使い方は一目瞭然です:
<Button variant="primary">Save</Button>
<Button variant="secondary">Cancel</Button>
<Button variant="outline">Details</Button>
.btn-primary クラスを保守する必要はありません。variant prop は TypeScript で型チェックでき、JSDoc でドキュメント化でき、エディタで自動補完が効きます。
以下のデモでは視覚的な出力を再現するために CSS クラスを使っています。実際のプロジェクトでは、バリアントのロジックはコンポーネントコード内に存在し、ユーティリティクラスがスタイリングを処理します。カスタム CSS クラスは作成しません。
コンポーネントコンポジション
複雑なレイアウトは、CSS クラスを追加するのではなく、小さなコンポーネントを組み合わせて構築します:
function UserList({ users }) {
return (
<div className="divide-y divide-gray-200">
{users.map((user) => (
<div key={user.id} className="flex items-center gap-4 py-3">
<Avatar src={user.avatar} size="sm" />
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 truncate">
{user.name}
</p>
<p className="text-sm text-gray-500 truncate">{user.email}</p>
</div>
<Badge variant={user.status}>{user.status}</Badge>
</div>
))}
</div>
);
}
各パーツ — <Avatar>、<Badge>、リストレイアウト — がコンポーネントです。.user-list__item、.user-list__avatar、.user-list__badge のようなクラス名は不要です。
コンポーネント内のレスポンシブパターン
ユーティリティフレームワークはレスポンシブな振る舞いにブレークポイントプレフィックスを使います。これらはコンポーネントのマークアップに直接記述します:
function ProductGrid({ products }) {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{products.map((product) => (
<ProductCard key={product.id} {...product} />
))}
</div>
);
}
グリッド用の別 CSS ファイルはありません。.product-grid や .product-grid--responsive クラスもありません。レスポンシブな振る舞いはインラインで宣言され、マークアップと同じ場所で確認できます。
コンポーネントが使えない場合
コンポーネントファーストのアプローチには、再利用可能なコンポーネントを作成できることが前提です。以下のような状況ではそれができません:
- CMS からサーバーレンダリングされた HTML — マークアップは固定で、スタイルシートの追加のみ可能
- サードパーティ UI フレームワーク — 変更できない固定の HTML を出力するライブラリ
- メールテンプレート — インラインスタイルとテーブルレイアウトに限定される
- ビルドステップのない静的 HTML サイト — コンポーネントフレームワークが利用できない
このような場合は、他の CSS 戦略にフォールバックします:
| 状況 | 推奨アプローチ |
|---|---|
| ビルドツールあり、HTML の変更不可 | CSS Modules または Tailwind @apply |
| ビルドツールなし、グローバル CSS のみ | BEM 命名規約 |
| 移行中のレガシーコードベース | 段階的なコンポーネント抽出 |
これらは例外であり、デフォルトではありません。React、Vue、Svelte、Astro、またはコンポーネントをサポートするフレームワークを使っているなら、コンポーネントファーストのアプローチをデフォルトの選択にしましょう。
メリットとデメリット
メリット
- 命名の判断が不要。 コンポーネント名が抽象化そのものです。
.card-headerや.btn-primaryを議論する必要はありません。 - 単一の信頼できる情報源。 スタイルとマークアップが1つのファイルに存在します。コンポーネントを変更すれば、使われているすべての場所が変わります。
- AI フレンドリー。 AI エージェントはプロジェクト固有の命名規約を推測する必要なく、ユーティリティベースのコンポーネントを確実に生成できます。
- CSS ファイル管理が不要。 別のスタイルシートも、未使用の CSS も、インポートチェーンもありません。
- Props が修飾子に取って代わる。
variant="primary"は.btn--primaryよりも明確で、TypeScript の型チェックもサポートします。 - デザインの一貫性。 ユーティリティはデザイントークンのスケールに紐づいており(例:
p-4=1rem、p-6=1.5rem)、一貫したスペーシングとサイジングを強制します。
デメリット
- クラスリストが冗長になる。 JSX 内の長いユーティリティ文字列は見づらくなることがあります。ただし 2026 年以降、コードは AI が書く時代です。
clsx/cnのようなヘルパーは人間の可読性のためのものであり、新規プロジェクトでわざわざ依存に追加する必要はありません。既存プロジェクトで使っているならそのまま使えますが、これから始めるなら不要です。 - 学習コスト。 ユーティリティフレームワークに慣れていない開発者は、その語彙を学ぶ必要があります。
- コンポーネントフレームワークが必要。 プレーン HTML/CSS 環境では適用できません(上記の例外を参照)。
コンポーネント階層の変数は不要
階層型デザイントークン戦略 — 3階層カラー戦略や3階層フォントサイズ戦略など — において、最も具体的なレベルはコンポーネント階層です。特定のコンポーネントにスコープされた CSS カスタムプロパティ、たとえば --_dialog-side-spacing、--_card-shadow、--_nav-font-size(--_ プレフィックスはローカルスコープを示す)のようなものです。これらの変数により、コンポーネントは自身のデザイン上の判断を CSS ファイル内でカプセル化できます。
コンポーネントファーストのアプローチでは、この階層は不要です。スコープ付きカスタムプロパティを定義するようなコンポーネントごとの CSS ファイルがないからです。スタイリングはコンポーネントのマークアップ内でユーティリティクラスとして直接表現され、コンポーネントフレームワーク自体(React、Vue、Svelte、Astro)がスコープの境界を提供します。
<Dialog> コンポーネントに --_dialog-side-spacing 変数は不要で、px-hsp-sm や px-hsp-md を直接使います(タイトトークン戦略で定義されたプロジェクトトークン)。<Card> に --_card-shadow は不要で、shadow-md を使います。コンポーネントファイル自体が、そのコンポーネントのスタイリング判断における単一の信頼できる情報源です。上位の階層(パレット、テーマ、スケール)はグローバルなデザイントークンとして引き続き存在します。コンポーネントアーキテクチャ自体がスコープを担うため、コンポーネント階層だけが不要になります。
一方、一般的な CSS アプローチ(BEM、CSS Modules)でスタイリングする場合は、コンポーネント階層の変数は依然として有用です。コンポーネントフレームワークによるスコープがない状況では、CSS カスタムプロパティはコンポーネントのスタイルシート内でデザイン上の判断をカプセル化するための数少ない手段の1つです。
使い分け
以下の場合は常にコンポーネントファーストのアプローチを使いましょう:
- プロジェクトがコンポーネントフレームワーク(React、Vue、Svelte、Astro、Solid など)を使っている
- プロジェクトがユーティリティ CSS フレームワーク(Tailwind CSS、UnoCSS など)を使っている
以下の場合のみ他のアプローチにフォールバックします:
- HTML を変更できない(CMS の出力、サードパーティウィジェット、レガシーマークアップ)
- コンポーネントフレームワークが利用できない(静的 HTML、メールテンプレート)
- プロジェクトが明示的に別の CSS 規約を選択しており、アプローチを混在させるべきでない
AI エージェント向けのルール
コンポーネントベースの Tailwind プロジェクトでコードを生成する場合:
- 常にコンポーネントを作成する — CSS クラスではなく
- ユーティリティクラスを直接使う — コンポーネントのマークアップ内で
- CSS Module ファイルやカスタムクラス名を生成しない — 明示的に求められない限り
- バリアントには props を使う —
.btn--primaryのような CSS 修飾子ではなく - コンポーネントを組み合わせる — 複雑な UI は小さなコンポーネントから構築する。CSS を増やすのではなく
よく使われるツール
- Tailwind CSS — 最も人気のあるユーティリティファーストフレームワーク。ビルドステップで使用している CSS のみを生成します。
- UnoCSS — オンデマンドで高速な代替フレームワーク。プラグインベースのアーキテクチャで、Tailwind プリセットと互換性があります。
- clsx / tailwind-merge — コンポーネント内で条件付きユーティリティクラスを組み合わせるためのヘルパー。