:has(): ファミリー セレクタ

時代が始まって以来(CSS の用語で)、Google はさまざまな意味でカスケードに取り組んできました。Google のスタイルは「カスケード スタイルシート」を構成します。セレクタもカスケード式になっています。横になっても構いません。ほとんどの場合、下方向に移動します。でも、決して上向きにはなりません。Google は長年にわたり、「親セレクタ」という機能を重視してきました。いよいよ、ついに実現します。:has() 疑似セレクタの形式。

:has() CSS 疑似クラスは、パラメータとして渡されたセレクタのいずれかが 1 つ以上の要素と一致する場合、要素を表します。

しかし、これは単なる「親」セレクタではありません。これはマーケティングの優れた方法です。あまり魅力的ではない方法として、「条件付き環境」セレクタがあるかもしれません。しかし、それとまったく同じ指輪がありません。「ファミリー」セレクタはどうでしょう。

対応ブラウザ

先に進む前に、ブラウザのサポートについて説明します。まだもう少しです。でも、あと少しです。Firefox はまだサポートされていませんが、今後サポートされる予定です。Safari にはすでに搭載されており、Chromium 105 でリリースされる予定です。この記事のすべてのデモで、使用しているブラウザでサポートされていないかどうかがわかります。

:has の使用方法

具体的にはどのようなものでしょうか。クラス everybody を持つ 2 つの兄弟要素がある次の HTML について考えてみましょう。クラス a-good-time の子孫を持つものを選択するには、どうすればよいでしょうか。

<div class="everybody">
  <div>
    <div class="a-good-time"></div>
  </div>
</div>

<div class="everybody"></div>

:has() では、次の CSS で実現できます。

.everybody:has(.a-good-time) {
  animation: party 21600s forwards;
}

これにより、.everybody の最初のインスタンスが選択され、animation が適用されます。

この例では、クラス everybody の要素がターゲットです。条件に、クラス a-good-time の子孫がある場合。

<target>:has(<condition>) { <styles> }

しかし、:has() は多くの機会を切り開くため、さらに先へ進むことができます。たとえ未知のものであっても、いくつか考えてみましょう。

figcaption を直接指定している figure 要素を選択します。 css figure:has(> figcaption) { ... } 直接 SVG の子孫を持たない anchor を選択する css a:not(:has(> svg)) { ... } 直接 input の兄弟要素を持つ label を選択します。横に向かいます!css label:has(+ input) { … } 子孫 imgalt テキストがない article を選択 css article:has(img:not([alt])) { … } DOM 内になんらかの状態が存在する documentElement を選択する css :root:has(.menu-toggle[aria-pressed=”true”]) { … } 子が奇数であるレイアウト コンテナを選択する css .container:has(> .container__item:last-of-type:nth-of-type(odd)) { ... } カーソルを合わせていないグリッド内の項目をすべて選択する css .grid:has(.grid__item:hover) .grid__item:not(:hover) { ... } カスタム要素を含むコンテナをすべて選択<todo-list> css main:has(todo-list) { ... } 次の直接の要素を含む 1 つまたは 2 つの段落をすべて選択 a articlehrcss p:has(+ hr) a:only-child { … }css article:has(>h1):has(>h2) { … }タイトルの後にサブタイトルが続く article を選択する css article:has(> h1 + h2) { … } インタラクティブな状態がトリガーされたときに :root を選択する css :root:has(a:hover) { … } figcaption がない figure に続く段落を選択する css figure:not(:has(figcaption)) + p { … }

:has() のどのような興味深いユースケースが思い浮かびますか?ここでの素晴らしい点は、メンタルモデルを打破するきっかけになったことです。これにより、「これらのスタイルに別のアプローチを取ることは可能か?」と思わせることができます。

使用例をいくつか見てみましょう。

カード

クラシック カードのデモをご覧ください。カードには、タイトル、サブタイトル、メディアなどの情報を表示できます。こちらがベーシック カードです。

<li class="card">
  <h2 class="card__title">
      <a href="#">Some Awesome Article</a>
  </h2>
  <p class="card__blurb">Here's a description for this awesome article.</p>
  <small class="card__author">Chrome DevRel</small>
</li>

メディアを紹介したいときはどうしますか?このデザインでは、カードを 2 列に分割できます。前に、この動作を表す新しいクラス(card--with-mediacard--two-columns など)を作成していました。このようなクラス名は呼び起こすのが難しくなるだけでなく、維持したり覚えたりすることが難しくなります。

:has() を使用すると、カードにメディアがあることを検出し、適切な処理を行うことができます。修飾子クラス名は必要ありません。

<li class="card">
  <h2 class="card__title">
    <a href="/article.html">Some Awesome Article</a>
  </h2>
  <p class="card__blurb">Here's a description for this awesome article.</p>
  <small class="card__author">Chrome DevRel</small>
  <img
    class="card__media"
    alt=""
    width="400"
    height="400"
    src="./team-awesome.png"
  />
</li>

そのままにする必要はありません。創造力を発揮できます。「注目」コンテンツを表示するカードは、レイアウト内でどのように調整できますか。この CSS は、注目のカードをレイアウトの全幅にして、グリッドの先頭に配置します。

.card:has(.card__banner) {
  grid-row: 1;
  grid-column: 1 / -1;
  max-inline-size: 100%;
  grid-template-columns: 1fr 1fr;
  border-left-width: var(--size-4);
}

バナー付きの注目カードが揺れて注意を引くことができる場合はどうすればよいでしょうか。

<li class="card">
  <h2 class="card__title">
    <a href="#">Some Awesome Article</a>
  </h2>
  <p class="card__blurb">Here's a description for this awesome article.</p>
  <small class="card__author">Chrome DevRel</small>
  <img
    class="card__media"
    alt=""
    width="400"
    height="400"
    src="./team-awesome.png"
  />
  <div class="card__banner"></div>
</li>

.card:has(.card__banner) {
  --color: var(--green-3-hsl);
  animation: wiggle 6s infinite;
}

可能性は無限大。

フォーム

フォームについてはどうですか?スタイルが難しいことで知られています。一例として、入力とそのラベルのスタイル設定があります。たとえば、フィールドが有効であることを示すにはどうすればよいでしょうか。:has() を使用すると、この作業がはるかに簡単になります。関連する形式の擬似クラス(:valid:invalid など)にフックできます。

<div class="form-group">
  <label for="email" class="form-label">Email</label>
  <input
    required
    type="email"
    id="email"
    class="form-input"
    title="Enter valid email address"
    placeholder="Enter valid email address"
  />   
</div>
label {
  color: var(--color);
}
input {
  border: 4px solid var(--color);
}

.form-group:has(:invalid) {
  --color: var(--invalid);
}

.form-group:has(:focus) {
  --color: var(--focus);
}

.form-group:has(:valid) {
  --color: var(--valid);
}

.form-group:has(:placeholder-shown) {
  --color: var(--blur);
}

次の例で試してみましょう。有効な値と無効な値を入力し、フォーカスのオンとオフを切り替えてみましょう。

:has() を使用して、フィールドのエラー メッセージの表示と非表示を切り替えることもできます。「email」フィールド グループにエラー メッセージを追加します。

<div class="form-group">
  <label for="email" class="form-label">
    Email
  </label>
  <div class="form-group__input">
    <input
      required
      type="email"
      id="email"
      class="form-input"
      title="Enter valid email address"
      placeholder="Enter valid email address"
    />   
    <div class="form-group__error">Enter a valid email address</div>
  </div>
</div>

デフォルトでは、エラー メッセージは非表示になっています。

.form-group__error {
  display: none;
}

ただし、フィールドが :invalid になり、フォーカスされていない場合は、追加のクラス名なしでメッセージを表示できます。

.form-group:has(:invalid:not(:focus)) .form-group__error {
  display: block;
}

ユーザーがフォームを操作したときに、ちょっとした気まぐれな雰囲気を加えることができなかった理由はありません。次の例をご覧ください。マイクロ インタラクションに有効な値を入力したときに視聴します。:invalid 値を指定すると、フォーム グループが振動します。ただし、ユーザーがモーションを好みていない場合に限られます。

Content

これについては、コードサンプルで触れました。では、ドキュメント フローで :has() をどのように活用できるでしょうか。たとえば、メディアを中心としたタイポグラフィのスタイル設定についてのアイデアが浮かび上がります。

figure:not(:has(figcaption)) {
  float: left;
  margin: var(--size-fluid-2) var(--size-fluid-2) var(--size-fluid-2) 0;
}

figure:has(figcaption) {
  width: 100%;
  margin: var(--size-fluid-4) 0;
}

figure:has(figcaption) img {
  width: 100%;
}

この例には図が含まれています。figcaption がない場合は、コンテンツ内でフローティングします。figcaption が存在する場合は、全幅を占有し、余分なマージンがあります。

状態への反応

マークアップの状態に応じてスタイルをリアクティブにするとどうなるでしょう。「クラシック」のスライド ナビゲーション バーを例に考えます。ナビゲーションを開く操作を切り替えるボタンがある場合は、aria-expanded 属性を使用できます。JavaScript を使用して適切な属性を更新できます。aria-expandedtrue の場合は、:has() を使用してこれを検出し、スライディング ナビゲーションのスタイルを更新します。JavaScript はその役割を担い、CSS はその情報を利用してさまざまな処理を実行できます。マークアップをシャッフルしたり、クラス名を追加したりする必要はありません(注: 本番環境に対応した例ではありません)。

:root:has([aria-expanded="true"]) {
    --open: 1;
}
body {
    transform: translateX(calc(var(--open, 0) * -200px));
}
をご覧ください。

ユーザーエラーを回避するために

これらの例の共通点は何でしょうか。どのクラスでも、:has() の使用方法が示されているだけでなく、クラス名を変更する必要はありませんでした。新しいコンテンツを挿入し、属性を更新しました。これは、ユーザーエラーを軽減できるという点で :has() の大きなメリットです。:has() を使用すると、CSS は DOM の変更に合わせて調整できます。JavaScript 内でクラス名を行き来する必要がなくなるため、デベロッパーによるエラーの可能性が低くなります。クラス名をタイプミスして Object ルックアップで維持しなければならないとき、誰もが経験したことがあるでしょう。

これは興味深い意見ですが、マークアップが明確化され、コードが少なくなるというメリットはありますか?JavaScript の調整が少ないため、JavaScript を少なくします。card card--has-media などのクラスが不要になるため、HTML が少なくて済みます。

既成概念にとらわれない考え方

前述のように、:has() はメンタルモデルを打破することを奨励します。さまざまなことを試す機会です。その一つが、CSS だけでゲームの仕組みを作ることです。フォームや CSS などを使用して、段階的な仕組みを作成できます。

<div class="step">
  <label for="step--1">1</label>
  <input id="step--1" type="checkbox" />
</div>
<div class="step">
  <label for="step--2">2</label>
  <input id="step--2" type="checkbox" />
</div>
.step:has(:checked), .step:first-of-type:has(:checked) {
  --hue: 10;
  opacity: 0.2;
}


.step:has(:checked) + .step:not(.step:has(:checked)) {
  --hue: 210;
  opacity: 1;
}

そこから興味深い可能性が広がります。これを使用して、フォームを走査して変換を行うことができます。このデモは別のブラウザタブで表示することをおすすめします。

定番のバズワイヤー ゲームはいかがですか?:has() を使用すると、メカニズムを簡単に作成できます。ワイヤーにカーソルを合わせると、ゲームオーバーです。はい。兄弟姉妹のコンビネータ+~)などを使用して、これらのゲーム メカニズムを作成できます。ただし、:has() を使用すると、興味深いマークアップの「トリック」を使わずに同じ結果を得ることができます。このデモは別のブラウザタブで表示することをおすすめします。

これらはすぐには本番環境に導入されませんが、プリミティブの使用方法を取り上げています。たとえば、:has() を連結できます。

:root:has(#start:checked):has(.game__success:hover, .screen--win:hover)
.screen--win {
  --display-win: 1;
}

パフォーマンスと制限事項

最後に、:has() でできないことは何ですか?:has() にはいくつかの制限があります。主な原因はパフォーマンス ヒットです。

  • :has():has() することはできません。ただし、:has() を連結することはできます。css :has(.a:has(.b)) { … }
  • :has() css :has(::after) { … } :has(::first-letter) { … } 内で疑似要素を使用しない
  • 複合セレクタ css ::slotted(:has(.a)) { … } :host(:has(.a)) { … } :host-context(:has(.a)) { … } ::cue(:has(.a)) { … } のみを受け入れる疑似内での :has() の使用を制限する
  • 疑似要素 css ::part(foo):has(:focus) { … } の後に :has() の使用を制限する
  • :visited の使用は常に false になります。css :has(:visited) { … }

:has() に関する実際のパフォーマンス指標については、こちらの Glitch をご覧ください。実装に関する知見や詳細を共有してくれた Byungwoo に感謝します。

以上です。

:has() に備えましょう。友人にこのことを知らせて、この投稿を共有してください。CSS に対する Google のアプローチ方法が大きく変わることでしょう。

すべてのデモは、こちらの CodePen コレクションにあります。