CSSが「思った通りに上書きされない」とき、原因の多くは詳細度(specificity)の理解不足です。前回までの記事で :is() / :where() を使った詳細度設計の話をしてきましたが、そもそも詳細度がどう計算されるのかを整理していなかったので、今回は図解中心にまとめます。


    詳細度は「3つの桁」でできている

    CSSの詳細度は、よく点数のように語られますが、実際は3つの独立した桁を持つカウンターです。

    • IDsな#header のようなID セレクタの数
    • class・属性・疑似クラスな.btn[type="text"]:hover などの数
    • type・疑似要素なdiva::before などの数

    下の図のように、これは(IDs, class等, type等)という3つの桁の組で表されます。

    詳細度3要素の重み比較図 ID・class/属性/疑似クラス・type/疑似要素の3つの要素が、詳細度の計算でそれぞれ独立した桁として扱われることを示す図 IDs #header class属性疑似クラス .btn [type] :hover type疑似要素 div ::before (1,0,0) (0,1,0) (0,0,1) > > どれだけ集めても、上の桁を1つも超えられない 例: .a.b.c.d.e.f.g.h.i.j(class 10個)でも #id(ID 1個)には絶対に勝てない

    重要なのは、これが「点数の合計」ではなく桁上がりしない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() :where() の詳細度比較図 4つの疑似クラス関数が、内側のセレクタリストの詳細度をどう外側に反映するかを示す図。:is()と:not()と:has()は内側で最も詳細度が高いものを採用し、:where()は常に0になる :is(.a, #b, span) 内側で最も高い詳細度を採用 この例なら #b の (1,0,0) :not(.a, #b, span) :is()と同じ採用ルール この例なら #b の (1,0,0) :has(.a, #b, span) :is()と同じ採用ルール この例なら #b の (1,0,0) :where(.a, #b, span) 中身に関係なく常に0 (0,0,0) 固定 使い分けの目安 :is() / :not() / :has() → グルーピングしつつ詳細度も持たせたい時 :where() → 詳細度を増やさずにセレクタをまとめたい・  あとから上書きしやすくしておきたい時 @scope(.theme-name) も実質 :where(.theme-name) と同じ詳細度0

    ポイントは、: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セレクタ 詳細度計算ツール CSSセレクタの詳細度(Specificity)を計算するオンラインツール。複数セレクタを一括計算。:is() / :not() / :has() / :where() / :nth-child(n of S) に対応。  続きを読む

    まとめ

    詳細度は「3つの桁のカウンター」というシンプルな仕組みですが、:is() 系と :where() の違いのように、モダンなセレクタ関数が絡むと一気に複雑に見えてきます。基本のIDs・class等・type等の3桁さえ押さえておけば、:is() は「中身の最大値を引き継ぐ」、:where() は「常に0」という2つのルールを足すだけで、ほとんどのケースは説明がつきます。

    これまでの3記事を通して、CSS Layersを諦めた経緯から、ITCSS/FLOCSSをベースにしたレイヤー設計、そして今回の詳細度の基礎まで一通り整理しました。実際にこの詳細度設計をどう運用しているかは、過去記事のコンポーネント実装例も合わせて読んでもらえると、より具体的にイメージしやすいかと思います。


    関連記事

    CSS設計|ITCSS×FLOCSSのハイブリッド構成と詳細度設計の実例 ITCSSとFLOCSSはどちらもCSS設計の代表的な手法ですが、性質も得意分野も異なります。両者の違いを整理しつつ、色トークン系ファイルの置き場に迷った実例や、`:is()` / `:where()` を使って詳細度を固定する6層構成のハイブリッド設計を紹介...  続きを読む CSSで「何番目か」を指定する:nth系セレクタとクラスベース位置選択の完全ガイド :nth-of-type・:nth-last-of-type の式の読み方から、:is() / :not() / :has() を使ったクラスベース位置選択まで網羅。「最初と最後以外」の複数の書き方と詳細度の比較も収録。CSS設計の実務に役立つリファレンス  続きを読む CSS Layersの使い方と注意点 - 実際に使って分かった問題点 CSS Layersは詳細度問題を解決する強力な仕組みですが、プラグインCSSとの共存に大きな壁があります。実プロジェクトで直面した問題点と、:is() / :where() を使った詳細度設計という代替アプローチを実例つきで解説します。  続きを読む