CSS Modules 戦略
問題
CSS はデフォルトでグローバル名前空間で動作します。記述したすべてのクラス名がどこからでもアクセスできるため、あるスタイルシートが別のスタイルを意図せず上書きする可能性があります。プロジェクトが成長すると、名前の衝突は避けられなくなります。2人の開発者が独立して .title クラスを作成したり、新機能の .container が既存のものと競合したりします。使われていない CSS も蓄積されていきます。コードベース全体を検索して未使用であることを確認しないと、安全にクラスを削除できないからです。
AI エージェントにとって、グローバル CSS は特に危険です。.card や .header クラスを生成するエージェントには、それらの名前がプロジェクト内に既に存在するかどうかを知る方法がありません。その結果、診断が困難な意図しないスタイルのオーバーライドが発生します。
解決方法
CSS Modules はグローバル名前空間の問題をビルド時に解決します。各 CSS ファイルはローカルスコープとして扱われ、クラス名は自動的にユニークな識別子(通常はハッシュを付加)に変換されるため、他のファイルのクラスと衝突することがありません。スタイルを JavaScript オブジェクトとしてインポートし、キーで参照します。
import styles from './Button.module.css';
function Button() {
return <button className={styles.primary}>Click me</button>;
}
ビルドツール(Webpack、Vite など)が .primary を .Button_primary_x7f2a のようなものに変換し、命名規約なしでユニーク性を保証します。作成した名前と生成された名前のマッピングは、インポートした styles オブジェクトを通じて自動的に処理されます。
コード例
クラス名のスコープの仕組み
CSS Modules ファイルを記述すると、ビルドツールが各クラス名をユニークなハッシュ付きバージョンに変換します。記述する元の名前は可読性のためのもので、ブラウザが見るのは生成された名前だけです。
基本的な使い方: インポートと適用
CSS Modules のワークフローでは、CSS ファイルを JavaScript モジュールとしてインポートします。インポートされたオブジェクトは、作成したクラス名を生成されたユニークな名前にマッピングします。
// Button.module.css
// .primary { background: #3b82f6; color: #fff; }
// .secondary { background: #e2e8f0; color: #1e293b; }
import styles from './Button.module.css';
function Button({ variant = 'primary', children }) {
return (
<button className={styles[variant]}>
{children}
</button>
);
}
コンポーネント間で衝突しない
CSS Modules の最大の利点は、異なるファイルで同じクラス名を使っても、生成される名前が異なることです。2つのコンポーネントがどちらも .title を使っても、衝突は起きません。
composes によるコンポジション
CSS Modules は composes キーワードをサポートしており、同じファイルや他のファイルからクラスを組み合わせることができます。これは CSS Modules での共通スタイルの共有方法で、重複を避けることができます。
/* shared.module.css */
.baseButton {
border: none;
padding: 10px 20px;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
}
/* Button.module.css */
.primary {
composes: baseButton from './shared.module.css';
background: #3b82f6;
color: #fff;
}
:global によるグローバルセレクタ
ローカルスコープされていないクラス名をターゲットにする必要がある場合があります。たとえば、サードパーティライブラリのクラスや body レベルの状態クラスなどです。CSS Modules はこの目的で :global() を提供しています。
/* デフォルトでローカルスコープ */
.container {
padding: 16px;
}
/* 特定のセレクタでスコープを解除 */
:global(.ReactModal__Overlay) {
background: rgba(0, 0, 0, 0.5);
}
/* ローカルとグローバルの混在 */
.container :global(.highlight) {
background: yellow;
}
他のアプローチとの比較
| アプローチ | スコープの仕組み | 命名 | ビルドツールが必要 | ランタイムコスト |
|---|---|---|---|---|
| CSS Modules | ビルド時のハッシュ | 作成者が選択、ローカルスコープ | はい | なし |
| BEM | 手動の命名規約 | 手動の規律(block__element--modifier) | いいえ | なし |
| ユーティリティファースト | カスタムクラス不要 | 定義済みのユーティリティ語彙 | 推奨 | なし |
| CSS-in-JS | ランタイムまたはビルド時 | JS コンポーネントと同じ場所に配置 | 場合による | ランタイムコストあり |
- BEM との比較: BEM は手動の命名規律で衝突回避を実現します。CSS Modules はそれを自動化します。
.titleと書くだけでツールがユニーク性を保証します。BEM は規約であり、CSS Modules は強制です。 - ユーティリティファーストとの比較: ユーティリティフレームワークはカスタムクラス名を完全に排除します。CSS Modules はセマンティックなクラス名を書きつつ自動的にスコープ化します。両者は共存可能で、コンポーネント固有のスタイルに CSS Modules、レイアウトにユーティリティを使うプロジェクトもあります。
- CSS-in-JS との比較: styled-components のようなライブラリは
<style>タグを注入してランタイムでスタイルをスコープ化します。CSS Modules はビルド時に同じスコープ化を実現し、ランタイムコストはゼロです。CSS-in-JS は props に基づく動的なスタイリングを提供しますが、CSS Modules では CSS カスタムプロパティまたは条件付きクラス名が必要です。
よくあるミス
:global を多用しすぎる
すべてを :global で囲むと、CSS Modules の目的が失われます。サードパーティのクラスや body レベルの状態をターゲットにする場合にのみ使い、利便性のために使うのは避けましょう。
/* 誤り: 理由なくスコープを解除 */
:global(.card) {
padding: 16px;
}
:global(.card-title) {
font-size: 18px;
}
/* 正しい: デフォルトでローカルスコープを使用 */
.card {
padding: 16px;
}
.cardTitle {
font-size: 18px;
}
composes を活用していない
composes がまさにこの目的のために存在するのに、複数の .module.css ファイルで共通スタイルを重複させてしまうケースです。
/* 誤り: ベーススタイルの重複 */
/* Button.module.css */
.primary {
border: none;
padding: 10px 20px;
border-radius: 6px;
background: #3b82f6;
color: #fff;
}
.secondary {
border: none;
padding: 10px 20px;
border-radius: 6px;
background: #e2e8f0;
color: #1e293b;
}
/* 正しい: 共通ベースを compose する */
.primary {
composes: base from './shared.module.css';
background: #3b82f6;
color: #fff;
}
.secondary {
composes: base from './shared.module.css';
background: #e2e8f0;
color: #1e293b;
}
グローバルスタイルとモジュールスタイルを1つのコンポーネントで混在させる
同じコンポーネントでグローバルスタイルシートと CSS Module の両方をインポートすると、どのスタイルがスコープされていてどれがされていないか曖昧になります。
// 誤り: スコープモデルの混在
import './global-card.css'; // グローバルスタイル
import styles from './Card.module.css'; // スコープされたスタイル
// 正しい: コンポーネントごとに1つのアプローチを一貫して使用
import styles from './Card.module.css';
JavaScript でケバブケースのクラス名を使う
CSS Modules はクラス名をオブジェクトのプロパティとしてエクスポートします。ケバブケースの名前はブラケット記法が必要になり、扱いにくくなります。
// 扱いにくい: ブラケット記法が必要
<div className={styles['card-title']}>
// 改善: .module.css でキャメルケースを使用
// .cardTitle { font-size: 18px; }
<div className={styles.cardTitle}>
使い分け
CSS Modules が適している場面:
- React、Vue、その他のフレームワークプロジェクト: ビルドツール(Webpack、Vite)が既に導入されている場合
- コンポーネントライブラリ: コンポーネントごとのスタイル分離が重要な場合
- グローバル CSS からの移行プロジェクト: CSS-in-JS やユーティリティフレームワークを採用せずにスコープ化したい場合
- 標準的な CSS を書きたいチーム: 自動的な衝突防止が必要だが、従来の CSS の書き方を維持したい場合
CSS Modules が適さない場面:
- ビルドツールが利用できない場合 — CSS Modules はクラス名を変換するバンドラーが必要です
- ユーティリティファーストのスタイリングを使いたい場合 — Tailwind や UnoCSS はカスタムクラス名の必要性を完全に排除します
- 高度に動的なスタイルが必要な場合 — スタイルが多くのコンポーネント props に依存する場合、CSS-in-JS の方が使いやすいことがあります