CSSが「思った通りに上書きされない」とき、原因の多くは詳細度(specificity)の理解不足です。前回までの記事で :is() / :where() を使った詳細度設計の話をしてきましたが、そもそも詳細度がどう計算されるのかを整理していなかったので、今回は図解中心にまとめます。
詳細度は「3つの桁」でできている
CSSの詳細度は、よく点数のように語られますが、実際は3つの独立した桁を持つカウンターです。
- IDsな
#headerのようなID セレクタの数 - class・属性・疑似クラスな
.btn、[type="text"]、:hoverなどの数 - type・疑似要素な
div、a、::beforeなどの数
下の図のように、これは(IDs, class等, type等)という3つの桁の組で表されます。
重要なのは、これが「点数の合計」ではなく桁上がりしない3進数のようなものだということです。class セレクタをどれだけ並べても、ID 1個には絶対に勝てません。.a.b.c.d.e.f.g.h.i.j(class 10個 = (0,10,0))と #id((1,0,0))を比べると、上の桁であるIDsの方が常に優先されるため、#id が勝ちます。
セレクタ例と詳細度の早見表
代表的なセレクタの詳細度をまとめると、次のようになります。
| セレクタ例 | 詳細度 (IDs, class等, type等) | 備考 |
|---|---|---|
div | (0,0,1) | type単体 |
.button | (0,1,0) | class単体 |
#header | (1,0,0) | ID単体 |
[type="button"] | (0,1,0) | 属性セレクタはclassと同格 |
a:hover | (0,1,1) | 疑似クラスはclassと同格 |
div::before | (0,0,2) | 疑似要素はtypeと同格 |
.button.is-active | (0,2,0) | classの数だけ加算 |
#header .nav a | (1,1,1) | 子孫すべて合算 |
!important はこの3つの桁の外側にある「最強の上書き」で、詳細度の計算ルール自体は変えません。詳細度で勝てない場合の最終手段として扱われます。
:is() / :not() / :has() / :where() の挙動の違い
モダンCSSでよく使う4つの疑似クラス関数は、詳細度の扱われ方がそれぞれ異なります。これを混同すると、「:where()で包んだのに詳細度が増えてしまった」のような事故につながります。
ポイントは、:is() :not() :has() の3つは「内側のセレクタリストのうち最も詳細度が高いもの」がそのまま外側の詳細度として採用されるのに対し、:where() だけは中身に関係なく常に詳細度0として扱われるという点です。:where(#header) のようにIDを入れても、詳細度は0のままになります。
@scope(.theme-name) { ... } も、スコープの起点を指定する部分自体の詳細度は :where() 相当(0)として扱われます。スコープによる影響範囲の限定という別の利点はありますが、詳細度の文脈では :where(.theme-name) とほぼ同じ挙動と考えて問題ありません。
数値には現れない例外たち
ここまでの数値計算とは別に、実務でつまずきやすい例外がいくつかあります。
:lang():hover:focus:placeholder-shownなどの疑似クラスな通常の疑似クラスと同様にclass等の桁(0,1,0)としてカウントされますが、「リセットCSSの中に紛れ込んでいる」と見落としやすく、想定より詳細度が高くなる原因になりがちです:nth-child():nth-of-type()などのnth系疑似クラスなこれも(0,1,0)としてカウントされますが、コンポーネントの境界条件を指定する場面で多用しやすく、気づかないうちに詳細度が積み上がっていきます::before::afterなどの疑似要素なtype等の桁(0,0,1)としてカウントされ、.box::beforeのような形で地味に詳細度を押し上げます- インラインスタイル(
style="...")なIDsよりさらに強い、別格の優先度として扱われます(詳細度の3桁の外側にある第0の桁、と捉えるとわかりやすいです)
これらは1つ1つの影響は小さいものの、リセットCSSやユーティリティクラスの中に積み重なると、想定外の場所で詳細度が崩れる原因になります。
CSSセレクタ 詳細度計算ツール
詳細度は、計算ツール等で確認することができます。
CSSセレクタ 詳細度計算ツール
まとめ
詳細度は「3つの桁のカウンター」というシンプルな仕組みですが、:is() 系と :where() の違いのように、モダンなセレクタ関数が絡むと一気に複雑に見えてきます。基本のIDs・class等・type等の3桁さえ押さえておけば、:is() は「中身の最大値を引き継ぐ」、:where() は「常に0」という2つのルールを足すだけで、ほとんどのケースは説明がつきます。
これまでの3記事を通して、CSS Layersを諦めた経緯から、ITCSS/FLOCSSをベースにしたレイヤー設計、そして今回の詳細度の基礎まで一通り整理しました。実際にこの詳細度設計をどう運用しているかは、過去記事のコンポーネント実装例も合わせて読んでもらえると、より具体的にイメージしやすいかと思います。
関連記事
CSS設計|ITCSS×FLOCSSのハイブリッド構成と詳細度設計の実例
CSSで「何番目か」を指定する:nth系セレクタとクラスベース位置選択の完全ガイド
CSS Layersの使い方と注意点 - 実際に使って分かった問題点

