CSS セレクタの詳細度を低く保つ - !important を使わないための設計

CSS が壊れる原因の大半は、詳細度(specificity)の衝突にある。あるスタイルを上書きしようとして !important を付け、それをさらに上書きするために別の !important を付け、最終的にどこで何が効いているのか誰にもわからなくなる。この悪循環を断ち切るには、そもそも詳細度を低く保つ設計が必要です。

詳細度の計算方法

詳細度は 3 つの桁で表現されます。

セレクタ種別詳細度
ID#header(1, 0, 0)
クラス・属性・擬似クラス.nav, [type], :hover(0, 1, 0)
要素・擬似要素div, ::before(0, 0, 1)

インラインスタイル(style 属性)はこれらすべてに優先し、!important はインラインスタイルすらも上書きします。詳細度は加算される仕組みなので、セレクタを長く書けば書くほど値が大きくなっていきます。

/* 詳細度 (0, 1, 0) */
.button { color: blue; }

/* 詳細度 (1, 1, 0) — 上の .button を必ず上書きする */
#sidebar .button { color: red; }

この例では #sidebar .button の詳細度が高いため、.button に対してどんなスタイルを後から書いても勝てません。上書きするには同等以上の詳細度が必要になり、セレクタがどんどん長くなっていきます。

ID セレクタを避ける

詳細度を低く保つうえで最も効果的なルールは、スタイリング目的で ID セレクタを使わないことです。ID セレクタはクラスセレクタ 10 個分に相当する詳細度を持つため、一度使うとそこだけ突出した詳細度が生まれてしまいます。

ID セレクタで書いた場合

ID に依存するとスタイルの上書きが困難になり、再利用性も失われる。

クラスセレクタで書いた場合

詳細度が均一に保たれ、記述順やファイルの読み込み順でスタイルを自然に制御できる。

ID は JavaScript のフック(getElementById など)やページ内リンクのアンカーとして使い、CSS のセレクタとしては使わないのが原則です。

/* 避ける */
#main-nav { display: flex; }
#main-nav #logo { width: 120px; }

/* こう書く */
.main-nav { display: flex; }
.main-nav-logo { width: 120px; }

セレクタのネストを浅く保つ

セレクタを深くネストすると、要素セレクタやクラスセレクタの加算で詳細度が上がるだけでなく、HTML の構造に強く依存したスタイルになります。

/* 詳細度 (0, 3, 1) — 深すぎる */
.page .content .article .title span {
  font-weight: bold;
}

/* 詳細度 (0, 1, 0) — フラットに書く */
.article-title-accent {
  font-weight: bold;
}

ネストが深いセレクタは HTML を少し変えただけでスタイルが外れるリスクがあります。クラス名で意味を表現し、セレクタは可能な限り 1〜2 階層に抑えるのが望ましい書き方です。

!important が必要になる状況を設計で潰す

!important を使いたくなる場面には共通のパターンがあります。

サードパーティ CSS との競合

外部ライブラリが高い詳細度のセレクタを使っている場合、自分のスタイルが負けてしまう。対策としては、ライブラリの CSS より後に自分の CSS を読み込み、同じ詳細度のセレクタで上書きする。

ユーティリティクラスの強制適用

.hidden { display: none; } のようなユーティリティが他のスタイルに負ける場合がある。これは例外的に !important を許容してよい数少ないケースだが、ユーティリティ以外では使わないと決めておく。

ユーティリティクラスに限定して !important を使う場合でも、チーム内でルールを明文化しておく必要があります。「どこでも自由に使ってよい」という状態が最も危険です。

/* ユーティリティのみ !important を許容する例 */
.u-hidden { display: none !important; }
.u-text-center { text-align: center !important; }

/* コンポーネントのスタイルでは絶対に使わない */
.card { padding: 16px; }
.card-header { font-size: 18px; }

:where() で詳細度をゼロにする

CSS の :where() 擬似クラスを使うと、内部のセレクタの詳細度をゼロにできます。リセット CSS やデフォルトスタイルの定義に非常に有用な機能です。

/* 詳細度 (0, 0, 0) — どんなクラスでも上書きできる */
:where(.dialog) {
  padding: 24px;
  border-radius: 8px;
}

/* 詳細度 (0, 1, 0) — 上のスタイルを自然に上書きする */
.dialog-compact {
  padding: 12px;
}

:where() と対になる :is() は、引数の中で最も高い詳細度を採用します。両者を意図的に使い分けることで、詳細度の階層を設計レベルでコントロールできます。

:where()

詳細度をゼロにする。ベーススタイルやリセットに向いている。

:is()

引数内の最大詳細度を採用する。通常のコンポーネントスタイルに適している。

詳細度の設計方針

詳細度を管理するうえで重要なのは、プロジェクト全体で詳細度の「層」を意識することです。下の層ほど詳細度が低く、上の層で必要に応じて上書きする構造を作ります。

:where() によるリセット・ベーススタイル(詳細度ゼロ)

クラスセレクタ 1 つのコンポーネントスタイル

状態変化のクラス付与(.is-active, .is-open など)

ユーティリティクラス(必要最小限の !important)

この順序を守れば、各層のスタイルが自然に上書きされ、!important に頼る場面はほぼなくなります。どうしても !important を書きたくなったら、それは詳細度の設計が破綻しているサインだと捉えてください。