🙄

CSS大解剖 13日目: 「セレクター 3/3」

に公開1

本稿は、2024年2月頃に書き溜めていたシリーズです。最後まで温存させるのが勿体ないので、未完成ですがそのまま公開します(公開日: 2025/9/26)。そのため、内容の重複や記述方針の不一致があるかもしれませんが、ご理解ください。


CSSの仕様を理解するために、1日ごとにテーマを決めて説明する企画13日目です。今日のテーマは前回に引き続き「セレクター」です。

詳細度

詳細度は、セレクターから構文的に決まる量です。

詳細度は、カスケード (宣言の優先度解決) において、「より一般的なルールをより限定的なルールで上書きする」という動作のために導入されているものです。たとえば、以下のようなケースでは詳細度に基づくカスケードがうまく動作します。

article h1 {
  font-size: 200%; /* こちらが優先される */
}
h1 {
  font-size: 150%;
}

しかし、複雑なスタイルでは詳細度に基づくカスケードが意図しない結果になったり、それによりより高い詳細度を求めてスタイル同士が競争する、言うなれば詳細度戦争のような状態になったりすることもあるでしょう。最近はスコープ化などカスケードのための新しい機構が色々と策定されつつあるので、それらの利用を検討してもいいでしょう。

ともあれ、詳細度の本来の意図は名前の通り「セレクターがどれくらい要素を限定するか」を相対的に定量化するものですが、技術的にはセレクターの詳細度はセレクターの振る舞いとは全く独立です。同じ振る舞いをするセレクターでも、詳細度は好きなように調整できます

  • 意味のない記述を増やすことで、詳細度を増やすことができます。 .foo.foo.foo にすれば詳細度は (0, 1, 0) 増えます。より一般には、 :is(*|*, <任意のセレクタ>) を付け足すことでほぼ自由に詳細度を増やせます。
  • :where() を使うことで、詳細度を減らすことができます。

詳細度の計算

詳細度は非負整数3つからなるベクトルで、 (1, 2, 3) のように表現します。詳細度の基本演算は以下の3つです。

  • 詳細度の加算は、要素ごとの足し算です。 (1, 2, 3) + (4, 5, 6) = (5, 7, 9) です。
  • 詳細度の比較は、辞書順による全順序比較です。たとえば、 (1, 2, 3) < (1, 3, 0) です。
  • 詳細度の最大は、辞書順で大きいほう (同率であればその値) を返します。たとえば、 (1, 2, 3) と (1, 3, 0) の最大は (1, 3, 0) です。
    • 特に、要素ごとの最大値ではないことに注意してください。(1, 2, 3) と (1, 3, 0) の最大は (1, 3, 3) ではありません。

詳細度の桁をそのまま並べて、 (1, 2, 3) を 123 のように表記する資料もありますが、この方法は各要素が10以上のときには使えないので避けておいたほうがいいでしょう。

セレクターの詳細度は以下のように計算します。

  • 単純セレクターの詳細度
    • 全称セレクターは (0, 0, 0) です。つまり、これらは詳細度に寄与しません。
    • 要素型セレクターの擬似要素セレクターの詳細度は (0, 0, 1) です。
    • 属性セレクター、クラスセレクター、および通常の擬似クラスセレクターの詳細度は (0, 1, 0) です。ただし、擬似クラスセレクターの詳細度にはいくつかの例外があります。
      • :is():not() の詳細度は、引数となっている複素セレクターの詳細度のうち最大のもの (同率であればその値) です。 :has() についても同様です。いずれも、擬似クラス自身の寄与はなく、引数のみから計算されます。
      • :where() の詳細度は、引数にかかわらず (0, 0, 0) です。
      • その他、セレクターを引数に取る擬似クラスは基本的に擬似クラス自身の寄与に加えて、引数の寄与も考慮して計算するのが一般的です。
    • IDセレクターの詳細度は (1, 0, 0) です。
  • 合成セレクターと複素セレクターの詳細度は、その中に出現する単純セレクターの詳細度の合計です。
  • セレクターリストの詳細度が必要な場合は、上の :is() / :not() でも説明したように、合成セレクターの詳細度のうち最大のもの (同率であればその値) として計算します。しかし、これについては後述の内容も参照してください。

セレクターリストと詳細度

セレクターリストの詳細度は、それに含まれる複素セレクターの詳細度の最大値として計算します。しかしセレクターリストの普通の使い道、つまり以下のような例では注意が必要です。

button, input[type="button" i], .btn {
  border: 1px gray outset;
}

.translucent {
  border: none;
}

ここで、 <button class="translucent"></button> にいずれの宣言が適用されるか考えてみます。以下は間違った計算例です。

  • 1つ目のスタイルルールは有効です。このセレクターリストの詳細度は (0, 1, 1) です。
  • 2つ目のスタイルルールは有効です。このセレクターリストの詳細度は (0, 1, 0) です。
  • したがって、1つ目の宣言が適用されます。

実は実際のCSSはこのように動作せず、スタイルルール内のセレクターリストの各要素は異なるルールを生成するのと同等として扱われます。つまり、以下のように展開してから評価したものとみなされます。

button {
  border: 1px gray outset;
}
input[type="button" i] {
  border: 1px gray outset;
}
.btn {
  border: 1px gray outset;
}

.translucent {
  border: none;
}

すると、先ほどの計算は以下のように訂正されます。

  • 1つ目のスタイルルールは有効です。このセレクターリストの詳細度は (0, 0, 1) です。
  • 2つ目のスタイルルールは無効です。
  • 3つ目のスタイルルールは無効です。
  • 4つ目のスタイルルールは有効です。このセレクターリストの詳細度は (0, 1, 0) です。
  • したがって、4つ目の宣言が適用されます。

あるいは、等価な解釈ですが、以下のようにも解釈できます。つまり、ある宣言の詳細度は、それを含むスタイルルールのセレクターリストの詳細度をそのまま使うのではなく、セレクターリストのうち対象要素がマッチした複素セレクターの中で最大の詳細度が使われます

スタイルルールをネストさせる場合の詳細度については、特別な規則があります

無効なセレクター

セレクターが構文エラーを生じた場合や、いくつかの決められた検査を通らなかった場合は、無効なセレクターとして扱われます。CSSは後方互換性と前方互換性の両方を重視しているため、セレクターが無効だった場合の扱いについても細かな指定があります。

スタイルルールにおける無効なセレクター

スタイルルールのセレクターリスト内に無効なセレクターがあった場合、スタイルルール全体が無効化されます。この挙動はあまりよい挙動ではありませんが、この挙動を悪用したスタイルがすでに広く使われているため、仕様が変更されることはないでしょう。

/* :is-a-button という擬似クラスはない。 button に対する指定も無効化される。 */
button, input:is-a-button {}

エラー寛容な擬似クラス

:is(), :where(), :has(), :nth-child(), :nth-last-child() 内のセレクターリストに無効なセレクターがあった場合、リストから無効な複素セレクターを全て除去します。残りの複素セレクターはそのまま使われ、エラーは外側には伝播されません。

/* :is-a-button という擬似クラスはないため input:is-a-button は無効だが、
 * button に対する指定は有効。
 */
:is(button, input:is-a-button) {}

この機能は便利そうに見えますが、思わぬ副作用を発生させる危険性もあります。それは、Webブラウザの機能実装のレベルによって、セレクターの詳細度が意図せず変わってしまうという可能性です。実際、上の例においてセレクターの詳細度は (0, 0, 1) ですが、もし :is-a-button が実装されたら詳細度は (0, 1, 1) になってしまいます。これは、buttonに当てられたスタイルのカスケード結果に影響を与える可能性があります。

なお、 :not() はこの限りではなく、内部のエラーをそのまま外部に伝播します。

特別なvendor prefix

互換性のため、::-webkit-* という形の擬似要素セレクターは全て構文上は有効とみなされます。実際に実装を提供する必要はなく、原則としては「何にもマッチしない」という挙動が期待されます。

/* ::-webkit-shenanigan は存在しないが有効と判定され、 .foo::first-line の適用を妨げない */
.foo::first-line, .foo::-webkit-shenanigan {
  color:red;
}

相対指定とネスト

相対セレクター

相対セレクターは以下のいずれかです。

  • 複素セレクター (例: ul > li)
  • 結合子(子孫結合子以外) + 複素セレクター (例: > li > a)

:has() 擬似クラス

:has()は名前の通り特定の条件を満たす要素を持っているかどうかを判定するセレクターであり、相対セレクターのリストを受け取ります。

:has() は各要素を起点に、内側の相対セレクターを実行します。このとき、内側の相対セレクターにマッチする要素が1つ以上あれば元の要素は残し、なければ元の要素は除外します。

相対セレクターが結合子で始まらない場合は、子孫結合子を仮定します。

/* <article><div><h1></h1></div></article> */
/* article h1 だと <h1> にマッチするが、以下は <article> 側が返ってくる */
article:has(h1)

/* <article><h1></h1></article> */
article:has(> h1)

/* li + li の逆 */
li:has(+ li)

:has() はネストできません。また、ほとんどの擬似要素は :has() 内では無視されます。

CSS Nesting

Working DraftであるCSS Nestingでは、スタイルルール内に別のスタイルルールを書くことを可能にします。これは完全な新機能ではなく既存構文の糖衣になるように設計されています。

ul.dropdown {
  border: 1px solid black;
  > li {
    list-style: none;
    + li {
      border-top: 1px solid #ccc;
    }
  }
}

ネストしたスタイルルールは、トップレベルのスタイルルールとは以下の点で異なります

  • 構文的曖昧性を回避するため、プレリュードを識別子で開始することができない。つまり、一部の要素型セレクターと一部の全称セレクターは開始位置では利用できない。
  • プレリュードはセレクターリストではなく相対セレクターのリストである。
  • 相対セレクター中に新しい単純セレクターである & を任意個含めることができる。

ネストしたスタイルルールは、以下の手順で脱糖されたものと同等に解釈されます。

  1. リスト内の相対セレクターを絶対セレクターに変換
    • 相対セレクターが結合子(子孫結合子以外)で始まる場合は、 & を前置する。 (例: > li& > li)
    • 相対セレクターが先頭の結合子を持たず & が出現しない場合は、 & と子孫結合子を前置する。 (例: li& li)
    • それ以外の場合 (相対セレクターが先頭の結合子を持たず & が出現する場合) は何もせず、そのまま絶対セレクターと解釈する。 (例: &:hover&:hover)
  2. セレクター内の & の出現をすべて :is(親ブロックのセレクター) で置き換える。
  3. ネストしたルールを、親ルールの直後に移動する。ネストしたルールが複数ある場合は、その相対順序は保存する。

このルールは以下のことを含意しています。

  • 親ルールのセレクターの詳細度は、ネストしない場合とは異なる方法で解釈される。
  • 親ルールに無効なセレクターがある場合の挙動は、ネストしない場合とは異なる。
  • 現行規格の規定では、親ルールのセレクターが擬似要素を選択している場合、それはネストしたルール内ではなかったことになる。
  • ネストしたルール内の宣言の、カスケードにおける「出現順」は、実際のソース内の出現順とは異なる場合がある。

Discussion

junerjuner

セレクターリストの詳細度は、それに含まれる複素セレクターの詳細度の最大値として計算します。しかしセレクターリストの普通の使い道、つまり以下のような例では注意が必要です。

それって css nesting では セレクターリストの詳細度が :is() 相当って話ってだけなので css nesting のときだけでは……?(そのあとの展開は css nesting 外なのでそうだが