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

設立以来(CSS の観点から)、私たちはさまざまな意味でカスケードを扱ってきました。スタイルは「カスケーディング スタイルシート」を構成します。セレクタもカスケードされます。横向きにしてもかまいません。ほとんどの場合、下降します。絶対上がらない。何年もの間、Google は「親セレクタ」を想像してきました。ついに登場!:has() 疑似セレクタの形式。

CSS 疑似クラス :has() は、パラメータとして渡されたセレクタのいずれかが 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) { ... } 直属の兄弟 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 が存在する場合、それらは全幅を占有し、追加のマージンを獲得します。

状態への対応

マークアップ内のなんらかの状態に、スタイルをリアクティブにしてみましょう。「クラシック」スライディング ナビゲーション バーナビゲーションを開く切り替えボタンがある場合は、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) { … }
  • 複合セレクタのみを受け入れる疑似内での :has() の使用を制限 css ::slotted(:has(.a)) { … } :host(:has(.a)) { … } :host-context(:has(.a)) { … } ::cue(:has(.a)) { … }
  • 疑似要素の後の :has() の使用を制限する css ::part(foo):has(:focus) { … }
  • :visited の使用は常に false になります css :has(:visited) { … }

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

これで完了です。

:has() に備えましょう。ぜひお友達に紹介して、この投稿を共有してください。CSS に対するアプローチに大きな変革をもたらすことでしょう。

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