書いたCSSが効かない。!important を付けたら直った。

複数のルールが同じ要素にぶつかったとき、ブラウザは決まった順番で勝ち負けを決めています。その判定の中心にあるのが、セレクタの強さを表す詳細度という値です。詳細度は「IDは100点、クラスは10点」のような点数の合計ではありません。合計だと思って数えていると合わない場面が発生します。

この記事では、詳細度とは何かを整理したうえで、3つの列で数える方法を確認します。あわせて、詳細度より先に決まってしまう判定と、!important を使う前に試せる手段をまとめます。

この記事で分かること
  • そもそも詳細度とは何か(どのルールを採用するかを決める、セレクタの強さの値)
  • 3列(ID / CLASS / TYPE)で数える方法と、点数の合計で考えると答えを間違える例
  • 詳細度より先に決まる判定があること(数えても無駄になる場面がある)
  • !important の前に試せる、弱い順の上書き手段

そもそも詳細度とは何か

詳細度とは、複数のCSSルールが同じ要素にぶつかったときに、どれを採用するかを決めるためのセレクタの強さを表す値です。ブラウザはセレクタがどれだけ具体的にその要素を指しているかを数値にして、大きいほうを採用します。.card p のようにクラスを含むセレクタは、p のような型セレクタだけのものより強くなります。

なぜこの値が必要なのか。1つの要素には、複数のルールが同時に当たることがあるからです。

HTML
<p class="lead">この文字は何色になるか</p>
CSS
p {
  color: black;
}

.lead {
  color: red;
}

p.lead はどちらもこの要素に当たっています。同じ color を違う値で指定しているので、ブラウザはどちらか一方を選ばなければなりません。

結果は赤になります。そしてこの2つを書く順番を入れ替えても、結果は赤のままです。「後に書いたほうが勝つ」とならないのは、ここで詳細度が働いているからです。

自分でCSSを書くときこの値を意識する場面は多くありません。意識するのは、思ったとおりに効かなかったときです。ただし、効かない理由が必ず詳細度にあるとは限りません。次の章で、まずそこを切り分けます。

CSSが効かない原因は2つに分かれる

「効かない」には2種類あります。書いたルールがそもそも当たっていないのか、当たっているが他のルールに負けているのか。詳細度が関係するのは後者です。まずここを切り分けます。

判定はブラウザの検証ツールで行います。

  1. 効かない要素を右クリックして「検証」を選ぶ
    Chromeの開発者ツールが開き、Stylesペイン(日本語表示では「スタイル」)にその要素へ当たっているルールが並びます。
  2. 書いたルールがStylesペインに出ているかを見る
    出ていなければ「当たっていない」状態です。詳細度の話ではありません。
  3. 出ているが、プロパティに打ち消し線が引かれている
    これが「当たっているが負けている」状態で、詳細度の話になります。

この切り分けを図にすると、次のようになります。

「当たっていない」状態の主な原因は次のとおりです。

  • CSSファイルが読み込まれていない
  • パスが誤っている
  • セレクタとHTMLが一致していない(クラス名のタイプミス・階層違い)
  • 文法エラーがあり、そこから先の記述が無効になっている
  • ブラウザキャッシュが古いままになっている

これらは詳細度以前の問題なので、この記事では扱いません。以降はルールは当たっているのに負けている場合の話に絞ります。

詳細度は「点数の合計」ではなく、3列を左から比べる

詳細度は1つの数字ではなく、ID列 / CLASS列 / TYPE列という3つの数字の組です。比較は左の列から順に行い、そこで差がついたら終了で右の列は見ません。数値の繰り上がりは行われません。

各列に何が入るかは次のとおりです。

入るもの
ID列IDセレクタ(#header1-0-0
CLASS列クラス(.card)/属性セレクタ([type="radio"])/擬似クラス(:hover :nth-of-type(3n)0-1-0
TYPE列型セレクタ(p h1)/擬似要素(::before ::placeholder0-0-1
どの列にも入らない全称セレクタ */結合子(> + ~ 半角スペース)0-0-0

間違えやすいのはCLASS列とTYPE列です。属性セレクタと擬似クラスはCLASS列擬似要素はTYPE列に入ります。::before は見た目がコロン2つで擬似クラスに似ていますが、数える場所が違います。

実際のセレクタで数えてみます。

CSS
/* .card(CLASS) + .title(CLASS) + span(TYPE) → 0-2-1 */
.card .title span {
  color: red;
}
CSS
/* #header(ID) + nav(TYPE) + a(TYPE) + :hover(CLASS) → 1-1-2 */
#header nav a:hover {
  color: red;
}
CSS
/* input(TYPE) + [type="text"](CLASS) + ::placeholder(TYPE) → 0-1-2 */
input[type="text"]::placeholder {
  color: red;
}

数えるのは要素の名前ではなく、セレクタに登場した部品の個数です。結合子(半角スペースや >)は数に入りません。

そして、数えた3列は合計せず、左から順番に比較します。

CLASS列で差がついた時点で、TYPE列が11でも100でも結果は変わりません。

「合計点」で考えると間違える2つの例

3列には桁の繰り上がりがありません。CLASS列がいくつになってもID列には影響せず、TYPE列がいくつになってもCLASS列には影響しません。つまり下位の列をいくつ積んでも、上位の列1つには勝てません

点数の合計で考えると、この2つで答えが食い違ってしまいます。

比較「点数の合計」で考えると実際に勝つのは理由
.text0-1-0) vs 型セレクタ11個(0-0-1110 対 11 で後者.textCLASS列で 1 対 0。ここで決着し、TYPE列は見られない
クラス11個(0-11-0) vs #main1-0-0110 対 100 で前者#mainID列で 0 対 1。ここで決着し、CLASS列は見られない

1つ目はそのまま貼って確かめられます。

HTML
<div><div><div><div><div><div><div><div>
  <p class="text">この文字は何色になるか</p>
</div></div></div></div></div></div></div></div>
CSS
/* html + body + div×8 + p = 型セレクタ11個 → 0-0-11 */
html body div div div div div div div div p {
  color: blue;
}

/* クラス1個 → 0-1-0 */
.text {
  color: red;
}

ブラウザで開くと文字は赤になります。CLASS列で先に差がついているため、TYPE列の11は比較されません。

:is() :not() :has() :where() の詳細度

これらの擬似クラスは、それ自体の重みを持ちません。括弧の中で最も詳細度の高い引数の重みをそのまま取ります。引数の合計ではありません。ただし :where() だけは別で、中に何を入れても常に 0-0-0 です。

セレクタ詳細度数え方
p:not(#fakeId)1-0-1引数の #fakeId が数えられ、そこに p が足される
:is(p, #fakeId)1-0-0引数の最大値を取る。p の分は足されない
h1:has(+ h2, > #fakeId)1-0-1:has() も同じ扱い
:where(#defaultTheme) a0-0-1:where() の中は0。a の分だけ残る

:where() の効き方は、実際に比べると分かりやすくなります。フッターが #defaultTheme の内側にある構造で試します。

HTML
<div id="defaultTheme">
  <footer><a href="/">このリンクは何色になるか</a></footer>
</div>
CSS
/* :where(...) は0。a の分だけで 0-0-1 */
:where(#defaultTheme) a {
  color: blue;
}

/* footer(TYPE) + a(TYPE) → 0-0-2 */
footer a {
  color: red;
}

#defaultTheme というIDが書かれているのに、フッター内のリンクは赤になります。ID列が0のままだからです。リセットCSSや共通スタイルを :where() で包んでおくと、後から普通のセレクタで上書きできます。

:is() :where() :has() はいずれも主要ブラウザで対応済みです。ただし :has() は比較的新しい擬似クラスで、主要ブラウザに出揃ったのは2023年末です(:is() :where() は2020〜2021年)。古い環境まで含めて動作を保証する案件では、対応状況を確認してから使ってください。

CSSのネストは :is() と同じ挙動になる

CSSのネストで書いた内側のルールは、:is() と同じように詳細度が決まります。外側に #id が混じっていると、内側のルールの詳細度が跳ね上がります。

CSS
p, #fakeId {
  span {
    /* 外側の最大値 #fakeId が効いて 1-0-1 */
    color: red;
  }
}

セレクタを並べて書いた親の中に1つでもIDがあると、その下のルール全部が「IDを含む詳細度」になります。ネストを書くときに気づきにくいところです。

なお、これはブラウザが解釈するCSSのネストの話です。Sassのネストはコンパイル時に p span, #fakeId span というセレクタリストへ展開されるため、#fakeId の外にある span0-0-2 のままです。

詳細度より先に決まる判定がある

「後に書いたほうが勝つはずなのに勝たない」という食い違いは、判定の順番を見ると解けます。

ブラウザは6段階で勝敗を決めており、詳細度が登場するのは5番目です。上の段で差がついていれば、詳細度は見られません。

判定内容
1オリジンと重要度ブラウザ標準/ユーザー/作者のどこで書かれたか、!important が付いているか
2コンテキストShadow DOM などのカプセル化
3要素に紐づくスタイルstyle 属性(インラインスタイル)
4カスケードレイヤー@layer で分けた層
5詳細度ここまでで決着しなかった場合に、3列で比較する
6出現順詳細度も同点なら、後に書かれたほうが勝つ

判定の流れを図にすると次のようになります。

上の段で差がついたら、下の段は見られません。 「後に書いたほうが勝つ」は6番目、つまり詳細度でも同点だったときにだけ働くルールです。

この表から読める実務的な帰結は3つです。

  • style 属性は3段目で決着する。CSSファイル側の詳細度をいくら上げても勝てない
  • !important 同士がぶつかったときは、同じレイヤー内なら詳細度が高いほうが勝つ
  • @layer が違えば詳細度は比較されない。負けた側の詳細度は結果に関係しない

継承された値は直接指定に勝てない

もう1つ、3列で比べること自体が起きない場面があります。継承です。

colorfont-family は、親要素に指定すると子要素にも引き継がれます。ただし引き継がれた値は、セレクタで指定された値ではないため、詳細度という値を持ちません。3列で比べる対象になっていない、ということです。

HTML
<div id="parent">
  <h1>この見出しは何色になるか</h1>
</div>
CSS
/* #parent(ID) → 1-0-0 */
#parent {
  color: green;
}

/* h1(TYPE) → 0-0-1 */
h1 {
  color: purple;
}

数字の上では #parent のほうが強いのですが、この見出しは紫になりますh1 を直接指定したルールがある以上、この要素の color はそちらで決まるためです。継承した値が使われるのは、その要素を指定したルールが1つもないときだけです。

「親に色を当てたのに子に反映されない」「IDで指定しているのに効かない」というときは、その子要素を直接指定しているルールがないかを確かめてください。

!important の前に試せる上書き手段4段階

!important は最終手段です。その手前に、副作用の小さい手段が4つあります。上から順に試します。

手段やること副作用
1出現順で勝つ同じ詳細度のまま、後に読み込まれる位置に書く読み込み順に依存する。ビルド構成が変わると崩れる
2セレクタを1段強くする親クラスを1つ足す等で必要最小限だけ上げる上げすぎると次の上書きがさらに難しくなる
3相手側を :where() で0にするリセットCSS・共通スタイル側を :where() で包む自分が書き換えられるCSSにしか使えない
4@layer で層を分ける上書きしたいスタイルを後ろの層に置く非対応ブラウザでは @layer ブロックごと無視される
!important最終手段次に上書きしたい人が !important を使うしかなくなる

3段目の :where() は、相手側のCSSを自分で書き換えられる場合にしか使えません。配布テーマやプラグインのCSSのように、自分の手が入れられないファイルに負けているときは、上書きの手段そのものより先に「どこに書けば安全か」「その変更がどこまで波及するか」を確かめる必要があります。既存サイトのCSSを触るときの進め方は、他人のCSSを壊さずに直す方法でまとめています。

!important が悪いわけではありません。書き換えられないCSSに当たったときには現実的な選択肢です。ただし一度使うと、次に上書きする場合も !important を使うしかなくなります。それを引き受けたうえで使うかどうかの判断になります。

そもそも詳細度で殴り合わずに済むよう、書くときの原則は3つです。

  • IDセレクタをスタイル指定に使わない
  • ネストを深くしない
  • !important を常用しない

この先は書き方ではなくCSS設計の話になります。詳しくは FLOCSSとは?基本の考え方と実際の書き方を分かりやすく解説 にまとめています。

詳細度はDevToolsで確認する

ここまで手で数える方法を書いてきましたが、実際の作業では毎回数える必要はありません。ブラウザが計算して表示してくれます。手で数えるのは仕組みを理解するときだけで十分です。

最初に見た打ち消し線の続きから、3ステップで確認します。

  1. Stylesペインで打ち消し線が引かれているルールを探す
    どのルールに負けているかが分かります。
  2. セレクタにマウスを乗せる
    現在のChromeでは、ツールチップにID列・CLASS列・TYPE列の3つの数値が表示されます(日本語表示では「詳細度:(1、0、2)」の形式で、内訳が「ID 相当」「クラス相当」「タイプ相当」と並びます)。この記事で数えてきた3列をそのまま読めます。
  3. Computedタブ(日本語表示では「計算済み」)を開く
    そのプロパティの最終的な値と、どのルールから来ているかを辿れます。

下の3枚は、先ほどの .text0-1-0)と型セレクタ11個(0-0-11)の例をDevToolsで開いたものです。

マウスを乗せる前の状態です。.text が勝ち、負けたほうのルールに打ち消し線が引かれています。

.text に乗せた状態です。3つの数値が 0-1-0 と並びます。

もう一方のセレクタに乗せた状態です。同じ位置に 0-0-11 と並びます。

Computedタブに切り替えた状態です。プロパティを展開すると、その値を指定しているルールがセレクタ単位で並びます。

よくある質問(FAQ)

!important を付けたのに効かないのはなぜですか?

相手側にも !important が付いている可能性があります。その場合は、同じレイヤー内なら詳細度が高いほうが勝ちます。また、インラインスタイルに !important が付いていると、CSSファイル側からは上書きできません。

親要素に色を指定したのに、子要素に反映されません。詳細度が足りないのでしょうか?

詳細度の問題ではありません。その子要素を直接指定したルールがあれば、継承されたスタイルより常に優先されます。親側の詳細度をどれだけ上げても結果は変わりません。

:where() は実務で使って大丈夫ですか?

主要ブラウザで対応済みです。リセットCSSや共通スタイルを :where() で包んで詳細度を0にしておくと、後から普通のセレクタで上書きできるようになります。

まとめ

  • 詳細度は3列(ID / CLASS / TYPE)を左から比べる。点数の合計ではない
  • 下位の列をいくつ積んでも上位の列1つに勝てない
  • 詳細度は判定の5番目。インラインスタイル・@layer!important は詳細度より先に決着する
  • 迷ったらDevToolsで確認する。手で数えるのは仕組みを理解するときだけでよい

数え方が確定すると、「なぜ効かないのか」を毎回勘で探さずに済みます。!important を足す前に、いま何段目で負けているのかを確かめてみてください。

関連記事

  • 自分で書き換えられないCSSに当たったとき、どこに書き、どこまで波及するかを確かめる
  • 詳細度を上げすぎない書き方の先。CSS設計で構造的に解決する
  • AIが出力したCSSに !important やIDセレクタが混ざっていないかの受け入れ観点
  • :has() / :where() を実際に使っている実装例