モードの変更

関係擬似クラス :has() でホバーしている要素の前の隣接要素を指定する

CSS

はじめに、 :has() を「関係擬似クラス」と記しているのは筆者訳である。

かつて CSS には「Determining the Subject of a Selector」という草案が存在した。これは子孫セレクタの最後以外をスタイル対象にできる特殊記号についての仕様だ。

CSS は通常、セレクタの最後にマッチする要素にスタイルが当たる。なので次のように記述すれば dl 要素の直下にある dd にスタイルが当たる。

dl > dd {
  font-weight: bold;
}
<dl>
  <dt>...</dt>
  <dd>スタイルが当たる</dd>
  <dd>スタイルが当たる</dd>
  <dd>スタイルが当たる</dd>
</dl>

また、dl 要素は直下に dt と dd を含んだ div を配置できるので、dt と dd が dl の直下にないマークアップもありえる。

<dl>
  <div>
    <dt>...</dt>
    <dd>スタイルは当たらない</dd>
    <dd>スタイルは当たらない</dd>
    <dd>スタイルは当たらない</dd>
  </div>
</dl>

こうなると先程の CSS のセレクタ dl > dd とは構造関係が異なるので dd にスタイルは当たらない。

さて、ここで「_div を直下に持つ dl にスタイルを当てたい_」となったら、どうすればよいだろう?

特定の構造関係を持つ親側の要素にマッチさせたいわけだが、先に説明した通りCSS は通常、セレクタの最後にマッチする要素にスタイルが当たるので、親側を識別するためのクラスが必要になる。

dl.-hasDiv {
  color: tomato;
}
<dl class="-hasDiv">
  <div>
    <dt>...</dt>
    <dd>スタイルは当たるが…</dd>
  </div>
</dl>

しかしこれでは子要素の構造関係に依存した指定ではなく、ただクラスに依存しただけの指定になってしまう。

これに対して、構造を子孫セレクタで表現した上でスタイルを当てる対象を特殊記号で指定する方法が考案された。それが「Determinig the Subject of a Selector」であった。HTML 側の構造をセレクタに起こすので、HTML に変更を加えずにスタイルを当てられるメリットが期待できた。

この草案は長い年月をかけて議論され、特殊記号を使う案ではなく :has() という関数型擬似クラスを使った案で標準化されつつある。

4.5. The Relational Pseudo-class: :has() - Selectors Level 4 - W3C Working Draft, 21 November 2018

2022 年 5 月 6 日現在の対応ブラウザは Safari 15.4 だけだが、Google Chrome 101 でも flag を有効にすれば利用可能だ。

:has() CSS relational pseudo-class | Can I use... Support tables for HTML5, CSS3, etc

関係擬似クラス :has()

:has() を仕様書通りに説明するとわかりにくいので、ユースケースからつかんでもらうのが良いだろう。

まず冒頭で話してたdiv を直下に持つ dl にスタイルを当てたいをやるなら次のようになる。

dl:has(> div) {
  color: tomato;
}
<!-- マッチする -->
<dl>
  <div>
    <dt>...</dt>
    <dd>...</dd>
  </div>
</dl>
<!-- マッチしない -->
<dl>
  <dt>...</dt>
  <dd>...</dd>
</dl>

「隣接要素に p を持つ h2」だと次のようになる。

h2:has(+ p) {
  color: tomato;
}
<!-- マッチする -->
<h2>...</h2>
<p>...</p>
<!-- h2 の隣接が img なのでマッチしない -->
<h2>...</h2>
<img />
<p>...</p>

もちろんタイプセレクタ以外も利用可能だ。次の指定では、「内容する最初の要素が img で、その次に a を持つ .title がある .card」にマッチする。

.card:has(> img:first-of-type + .title a) {
  color: tomato;
}
<!-- マッチする -->
<div class="card">
  <img />
  <h2 class="title"><a>...</a></h2>
  <p>...</p>
</div>
<!-- .title 内に a がないのでマッチしない -->
<div class="card">
  <img />
  <h2 class="title">...</h2>
</div>
<!-- 直下の最初の要素が img ではないのでマッチしない -->
<div class="card">
  <h2 class="title"><a>...</a></h2>
  <img />
  <p>...</p>
</div>

:has() を複数使うと AND 条件のように扱える。

.card:has(.__actions):has(.__footer) {
  color: tomato;
}
<!-- マッチする -->
<div class="card">
  <div class="__body">...</div>
  <div class="__actions">...</div>
  <div class="__footer">...</div>
</div>
<!-- .__actions がないのでマッチしない -->
<div class="card">
  <div class="__body">...</div>
  <div class="__footer">...</div>
</div>

他の擬似クラスとも組み合わせられる。例えば :not() と入れ子にすると :has() を NOT 条件にできる。

dl:not(:has(> div)) {
  color: tomato;
}
<!-- 直下に div があるのでマッチしない -->
<dl>
  <div>
    <dt>...</dt>
    <dd>...</dd>
  </div>
</dl>
<!-- 直下に div がないのでマッチする -->
<dl>
  <dt>...</dt>
  <dd>...</dd>
</dl>

このように E:has(S) のような構文において、構造関係 S を満たす E にマッチする。SE 内包する子要素の構造も指定できるし、隣接要素や論理擬似クラスなどで E が置かれている状況も指定できる。まさに可能性の獣のような擬似クラスであることがわかると思う。

あの頃はまだ :has() はなかった

実は約 10 年前に Determinig the Subject of a Selector についての記事を書いている。

:hover している要素の直前の要素の指定を CSS4 の!符号で妄想した

当時の僕は Chris Coyier 氏の『Stairway Nav』という記事のデモを見て Determinig the Subject of a Selector での実現を妄想していた。

See the Pen Stairway Hover Nav by Chris Coyier (@chriscoyier) on CodePen.

上記埋め込みは Chris Coyier 氏が作ったデモだ。ID セレクタだったり @import "compass/css3"; していたり時代を感じる。10 年以上ずっと残ってるのもえらい。さすが CodePen の作者。

Navigation の一つにホバーすると、その前後の要素もスタイルが変化するのがわかる。当時の CSS セレクタにはホバーした前の要素を得る方法がなかったので、氏はこれを jQuery の .eq() メソッドを使った動的なクラス付与で実現している。

:has() を使えば jQuery なしで同じことができるはずだ。10 年越しの「それ CSS でできるよ」である。

10 年かかったけどそれ CSS でできるよ

というわけで Fork して来上がったものが次のデモだ。Safari 15.4 で見てほしい。

See the Pen CSS Stairway Hover Nav by oti (@otiext) on CodePen.

リファクタの余地は多々あるが、元の HTML を崩したくなかったのと、Fork の主眼がセレクタの改良なのでプロパティ設計の良し悪しには目を瞑ってもらいたい。それでもステップ識別を ID セレクタから短いクラスセレクタに変えたり、ステップごとに明るくなる背景色の表現に hsl() を使ったりはしている。

改めて言うが、従来の CSS セレクタではホバーした要素に対して、自身か、自身の子孫か、自身の後の隣接要素にしかスタイルを当てられなかった。

:has() を使えばそれらの逆、つまり「自身の親」や「自身の前の隣接要素」にスタイルが当てられるようになる。

「ホバーした要素の前の要素」であれば、ホバーされている要素を隣接に持つという条件なので、次にようになる。

E:has(+ E:hover) {
  color: tomato;
}

「ホバーした要素の 2 つ前の要素」であれば、:has() の中は「ホバーされている要素を隣接に持つ要素を隣接に持つ」なので、次のようになる。

E:has(+ E + E:hover) {
  color: tomato;
}

3 つ前でも 4 つ前でもこのパターンで延々と書ける。すごい。


10 年前に夢見た CSS は当時とは形を変えたが、ちゃんと叶った。CSS はついに DOM ツリーをさかのぼる手段を手に入れたのだ。

今はまだ Safari 15.4 でしか動かないが、ライフサイクルの早い他ブラウザが stable で対応するのもそう遠くはないだろう。

CSS の進化に乾杯!