CSS のネスト深度を制限する - 3階層ルールの実践
CSS のセレクタが深くネストされると、詳細度が上がり、スタイルの上書きが困難になります。さらにリファクタリング時に HTML 構造への依存が足かせとなり、変更のたびに連鎖的な修正が発生します。
ネストが深いコードの問題
たとえば以下のような CSS を考えます。
.page .content .sidebar .widget .title {
font-size: 14px;
color: #333;
}このセレクタは 5 階層です。一見すると「sidebar の中の widget の title」という意図が読み取れますが、実際には複数の問題を抱えています。
5 階層のクラスセレクタは詳細度が 0-5-0 になります。このスタイルを別の場所で上書きするには、同等以上の詳細度が必要です。結果として !important の乱用につながります。
.page .content .sidebar という階層が HTML 上に存在しなければスタイルが適用されません。レイアウトを変更して .sidebar を .content の外に移動した瞬間、スタイルが壊れます。
3 階層ルールとは
セレクタのネストを最大 3 階層に制限するというルールです。ここでいう「階層」は、セレクタに含まれるクラス・要素・属性などの段数を指します。
.card .header .title のように、コンポーネントのルート・子要素・孫要素の 3 段で収まっている状態。詳細度は 0-3-0 以下に保たれる。
.page .content .card .header .title のように、ページ構造の文脈がセレクタに漏れている状態。コンポーネントが配置場所に依存してしまう。
このルールの背景にあるのは、コンポーネントは自身のスコープ内で完結するべきだという考え方です。3 階層あればルート・子・孫の関係を表現でき、ほとんどのコンポーネントはこの範囲に収まります。
具体的なリファクタリング
先ほどの 5 階層セレクタを 3 階層以内に書き直します。
/* Before: 5階層 */
.page .content .sidebar .widget .title {
font-size: 14px;
color: #333;
}
/* After: 1階層 */
.widget-title {
font-size: 14px;
color: #333;
}.widget-title という単一クラスにすることで、詳細度は 0-1-0 まで下がります。HTML のどこに配置しても同じスタイルが適用され、構造への依存がなくなります。
もう少し文脈が必要な場合でも、3 階層以内に収めます。
/* 2階層: widget の中の title */
.widget .title {
font-size: 14px;
color: #333;
}
/* 3階層: sidebar 内の widget だけ変える場合 */
.sidebar .widget .title {
font-size: 12px;
}3 階層目が登場するのは、同じコンポーネントが配置場所によってスタイルを変える必要があるときに限られます。
Sass/SCSS でのネスト制御
Sass を使っていると、ネストの書きやすさが逆に深いセレクタを生みやすくなります。
/* やりがちなSCSS */
.page {
.content {
.sidebar {
.widget {
.title {
font-size: 14px;
}
}
}
}
}このコードはコンパイル後に .page .content .sidebar .widget .title という 5 階層セレクタになります。SCSS 上ではインデントが整理されて見やすく感じますが、出力される CSS は先ほどと同じ問題を持っています。
Stylelint の max-nesting-depth ルールを使うと、SCSS のネスト深度を自動で検出できます。
max-nesting-depth: 2 と設定すると、3 階層を超えるネストで警告が出る。
{
"rules": {
"max-nesting-depth": 2
}
}この設定では、SCSS のネストが 2 段(=出力セレクタ 3 階層)を超えるとエラーになります。
3 階層を超えそうなケースへの対処
実際のプロジェクトでは、状態の変化やバリエーションの表現で階層が増えがちです。
/* 4階層になってしまう例 */
.nav .menu .item.is-active {
background: #eee;
}.item.is-active は 2 つのクラスが同一要素に付いている形ですが、セレクタ全体では .nav .menu .item.is-active で 4 つのクラスを含み、詳細度は 0-4-0 です。
/* 対処: コンポーネント名を明示して階層を減らす */
.nav-item.is-active {
background: #eee;
}.nav と .menu という親の文脈をセレクタから外し、.nav-item というコンポーネント名に情報を集約しています。詳細度は 0-2-0 に収まります。
| パターン | 階層数 | 詳細度 |
|---|---|---|
| .page .content .sidebar .widget .title | 5 | 0-5-0 |
| .sidebar .widget .title | 3 | 0-3-0 |
| .widget-title | 1 | 0-1-0 |
| .nav-item.is-active | 1(2クラス同一要素) | 0-2-0 |
階層数を減らすほどセレクタは HTML 構造から独立し、再利用性が高まります。3 階層ルールはその目安として、チーム内で共有しやすい明確な基準になります。