zudo-css

Type to search...

to open search from anywhere

コンポーネントファースト戦略

作成2026年3月13日更新2026年3月27日Takeshi Takatsudo

問題

プロジェクトが 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 クラス(やってはいけない例)

見た目は問題ありませんが、命名の判断、別の 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 クラスは作成しません。

Props によるコンポーネントバリアント

コンポーネントコンポジション

複雑なレイアウトは、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 = 1remp-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-smpx-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 プロジェクトでコードを生成する場合:

  1. 常にコンポーネントを作成する — CSS クラスではなく
  2. ユーティリティクラスを直接使う — コンポーネントのマークアップ内で
  3. CSS Module ファイルやカスタムクラス名を生成しない — 明示的に求められない限り
  4. バリアントには props を使う.btn--primary のような CSS 修飾子ではなく
  5. コンポーネントを組み合わせる — 複雑な UI は小さなコンポーネントから構築する。CSS を増やすのではなく

よく使われるツール

  • Tailwind CSS — 最も人気のあるユーティリティファーストフレームワーク。ビルドステップで使用している CSS のみを生成します。
  • UnoCSS — オンデマンドで高速な代替フレームワーク。プラグインベースのアーキテクチャで、Tailwind プリセットと互換性があります。
  • clsx / tailwind-merge — コンポーネント内で条件付きユーティリティクラスを組み合わせるためのヘルパー。

参考リンク

Revision History