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

古くから(CSS の用語で言えば)、さまざまな意味でカスケードを使用してきました。スタイルは「カスケーディング スタイル シート」を構成します。セレクタもカスケードします。横方向に移動することもあります。ほとんどの場合、下向きになります。ただし、上昇することは決してありません。長年、Google は「親セレクタ」の開発を夢見てきました。いよいよリリースが近づいてきました。:has() 疑似セレクタの形で指定します。

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

ただし、これは単なる「親」セレクタではありません。良いマーケティング方法ですね。あまり魅力的ではない方法としては、「条件付き環境」セレクタがあります。でも、それはちょっと違う響きです。「family」セレクタはどうですか?

対応ブラウザ

先に進む前に、ブラウザのサポートについて説明します。まだ完了していません。ただし、近づいてきています。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) { ... } 直接的な兄弟 hr 要素を持つ段落内の単独の a をすべて選択 css p:has(+ hr) a:only-child { … } 複数の条件が満たされる article を選択 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 値を指定すると、フォーム グループが揺れます。ただし、ユーザーがモーション設定を行っていない場合に限られます。

コンテンツ

この点については、コードサンプルで説明しました。では、ドキュメント フローで :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 が存在する場合、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() の大きなメリットです。: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) { … } 内で疑似要素を使用しない
  • 複合セレクタのみを受け入れる疑似要素内の :has() の使用を制限。 css ::slotted(:has(.a)) { … } :host(:has(.a)) { … } :host-context(:has(.a)) { … } ::cue(:has(.a)) { … }
  • 疑似要素 css ::part(foo):has(:focus) { … } の後の :has() の使用を制限
  • :visited を使用すると、常に false になります。 css :has(:visited) { … }

:has() に関連する実際のパフォーマンス指標については、こちらのグリッチをご覧ください。実装に関するこれらの分析情報と詳細を共有してくれた Byungwoo に感謝します。

これで完了です。

:has() の準備をします。ぜひお友達に教えて、この投稿を共有してください。CSS の活用方法に革命をもたらす機能です。

すべてのデモは、こちらの CodePen コレクションで確認できます。