CSSの:has()疑似クラスの便利な使い方のまとめ
Post on:2022年9月8日
CSSの:has()
疑似クラスが主要ブラウザでサポートされ、喜んでいる人も多いと思います。今まではJavaScriptを使用しなければできなかったことが、:has()
疑似クラスを使用するとさまざまなセレクタを条件式のように記述できます。
たとえば、子に画像がある場合とない場合、子の数が奇数の場合と偶数の場合、セレクタを追加したり変更することなく指定できます。また、コンテンツやフォームなどにも便利な使い方がたくさんあります。
:has(): the family selector
by Jhey Tompkins
下記は各ポイントを意訳したものです。
※元サイト様のライセンスに基づいて翻訳しています。基づいてというのは、貢献部分に関して同ライセンスも含みます。
先日リリースされたChrome 105, Edge 105で:has()
疑似クラスはサポートされ、コンテナクエリ(@container
)と:modal
疑似クラスもサポートされました。
- レスポンシブの実装が今までと変わる! CSSの新機能コンテナクエリと:has()疑似クラス、最初に理解しておきたい基礎知識を解説
- モーダル要素の実装に便利なCSSの新機能「:modal疑似クラス」、主要ブラウザのすべてにサポートされました
- CSSの:has()疑似クラスの便利な使い方を徹底解説
下記は各ポイントを意訳したものです。
※元サイト様のライセンスの元、翻訳しています。
- はじめに
- :has()疑似クラスのサポートブラウザ
- :has()疑似クラスの基本的な使い方
- :has()疑似クラスの便利な使い方
- :has()疑似クラスは、ユーザーエラーを回避するのに役立つか
- 枠にとらわれない考え方
- :has()疑似クラスのパフォーマンスと制限
- 終わりに
はじめに
CSSの登場以来、わたし達はさまざまな意味でカスケードを使用してきました。実装に使用するCSSは「カスケードスタイルシート(Cascading Style Sheet)」を構成しています。そして、セレクタもカスケードします。横に進むこともできますが、ほとんどの場合は下に進みます。決して上に進むことはありません。
何年もの間、わたし達は上に進む「親」セレクタを求めていました。
そしてついに、:has()
疑似クラスでそれが実現しました!
CSSの:has()
疑似クラスとは、パラメータとして渡されたセレクタのいずれかが少なくとも一つの要素に一致する場合に、その要素を表します。
しかし、これは単なる「親」セレクタではありません。別の言い方にすると、「条件付き環境」セレクタでしょうか。うーん、ひょっと響きが違いますね。「家族」セレクタ(family selector)の方がいいかもしれません。
:has()疑似クラスのサポートブラウザ
:has()
疑似クラスの使い方の前に、ブラウザのサポート状況について触れておきましょう。まだ十分なサポート状況とは言えませんが、すべてのブラウザでサポートされるのはもうすぐです。Safariでは15.4ですでにサポートされており、Chromeでは先日の105でサポートされました。Firefoxではロードマップ上にありますが、まだサポートされていません。
この記事のすべてのデモには:has()
疑似クラスが使用されています。まずは、下記のデモページであなたのブラウザが:has()
疑似クラスをサポートしているかどうか確認してください。
See the Pen
Is :has() supported here? by web.dev (@web-dot-dev)
on CodePen.
2022年9月現在、:has()
疑似クラスのサポート状況は、下記の通りです。
:has()疑似クラスの基本的な使い方
:has()
疑似クラスの基本的な使い方を次のHTMLを例に見てましょう。
.everybody
を持つ2つの兄弟要素があります。
1 2 3 4 5 6 7 |
<div class="everybody"> <div> <div class="a-good-time"></div> </div> </div> <div class="everybody"></div> |
.a-good-time
の子孫要素を含む方のセレクタはどのように記述すればよいでしょうか?
:has()
疑似クラスを使用すると、それが簡単にできます。
1 2 3 |
.everybody:has(.a-good-time) { animation: party 21600s forwards; } |
このCSSで、.everybody
の1つ目の要素が選択され、スタイルが適用されます。
この例では、.everybody
というクラスを持つ要素がターゲットになっています。条件は、.a-good-time
を持つ子孫を持つことです。
1 |
<target>:has(<condition>) { <styles> } |
:has()
疑似クラスはこれだけではありません!
ほかにもいろいろな使い方があるので、見てましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
/* figcaptionがあるfigure要素を選択 */ figure:has(> figcaption) { ... } /* svgの直接の子孫がないa要素を選択 */ a:not(:has(> svg)) { ... } /* inputの直接の兄弟があるlabel要素を選択 */ label:has(+ input) { … } /* 子孫のimgにaltがないarticle要素を選択 */ article:has(img:not([alt])) { … } /* DOM内で何らかの状態が存在するdocumentElementを選択 */ :root:has(.menu-toggle[aria-pressed=”true”]) { … } /* 子の数が奇数の.containerを選択 */ .container:has(> .container__item:last-of-type:nth-of-type(odd)) { ... } /* ホバーされていないグリッド内のすべてのアイテムを選択 */ .grid:has(.grid__item:hover) .grid__item:not(:hover) { ... } /* カスタム要素<todo-list>を含むコンテナを選択 */ main:has(todo-list) { ... } /* 直接のhrの兄弟があるp要素内のすべてのa要素を選択 */ p:has(+ hr) a:only-child { … } /* 複数の条件を満たすarticle要素を選択 */ article:has(>h1):has(>h2) { … } /* h1の後にh2が続くarticle要素を選択 */ article:has(> h1 + h2) { … } /* インタラクティブな状態が発生したときに:rootを選択 */ :root:has(a:hover) { … } /* figcaptionがないfigureに続くp要素を選択 */ figure:not(:has(figcaption)) + p { … } |
:has()
疑似クラスの興味深い使用例はありましたか?
ここで注目すべきは、メンタルモデルを壊すことを奨励していることです。「これらのスタイルに別の方法でアプローチできないか」と考えさせられます。
:has()疑似クラスの便利な使い方
では:has()
疑似クラスを実際にどのように使用するのか、いくつか例を見てみましょう。
カードの実装
まずはシンプルなカードのデモを見てみましょう。
カードにはタイトルやサブタイトル、画像などのメディアなど、あらゆる情報を配置できます。カードの基本的なHTMLは下記の通りです。
1 2 3 4 5 6 7 |
<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> |
実際の動作は、デモをご覧ください。
See the Pen
Basic Card by web.dev (@web-dot-dev)
on CodePen.
カードに画像を配置したいときはどうしますか?
カードでは2つのカラムを使用できます。今まではcard--with-media
やcard--two-columns
のようなクラスを作成していたかもしれません。このようなクラス名は、思いつくのが難しいだけでなく、それをずっと覚えておくのも難しいものです。
:has()
疑似クラスを使用すれば、カードが何らかのメディアを持っていることを検出して、適切なスタイルを適用できます。修飾子(Modifier)のクラス名は必要ありません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<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> |
実際の動作は、デモをご覧ください。
See the Pen
Card with Media by web.dev (@web-dot-dev)
on CodePen.
これだけではありません、もっとクリエイティブになれるはずです。特集コンテンツを表示するカードは、レイアウト内でどのように適応させますか? 下記のCSSは、特集を表示するカードをレイアウトの幅いっぱいに表示し、グリッドの最初に配置します。
1 2 3 4 5 6 7 |
.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); } |
実際の動作は、デモをご覧ください。
See the Pen
:has Media Card Layouts by web.dev (@web-dot-dev)
on CodePen.
さらに、バナーがある特集カードで注目を集めるためにアニメーションさせてみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<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> |
アニメーションは6秒間隔で動かしてみます。
1 2 3 4 |
.card:has(.card__banner) { --color: var(--green-3-hsl); animation: wiggle 6s infinite; } |
実際の動作は、デモをご覧ください。
See the Pen
:has Wiggle Card by web.dev (@web-dot-dev)
on CodePen.
:has()
疑似クラスには多くの可能性があります!
フォームの実装
フォームはCSSでスタイルするのが難しいことで知られています。たとえば、入力欄とそのラベルのスタイルです。入力が有効であることを示すにはどうすればよいでしょうか?
:has()
疑似クラスを使用すると、これが非常に簡単になります。:valid
や:invalid
など、関連するフォームの疑似クラスをフックにできます。
1 2 3 4 5 6 7 8 9 10 11 |
<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> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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); } |
実際の動作は、デモをご覧ください。
有効な値と無効な値を入力し、フォーカスをオンまたはオフにしてみてください。
See the Pen
:has some <input> by web.dev (@web-dot-dev)
on CodePen.
さらに、:has()
疑似クラスを使用して、エラーメッセージを表示・非表示することもできます。Emailのフィールドグループを使用して、エラーメッセージを追加してみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<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> |
デフォルトでは、エラーメッセージを非表示にします。
1 2 3 |
.form-group__error { display: none; } |
そしてフィールドが:invalid
になり、かつフォーカスされていない場合、追加のクラス名を必要とせずにメッセージを表示できます。
1 2 3 |
.form-group:has(:invalid:not(:focus)) .form-group__error { display: block; } |
実際の動作は、デモをご覧ください。
See the Pen
This form shows and hides its error messages by web.dev (@web-dot-dev)
on CodePen.
さらにフォームにセンスのいいアクションを加えてみます。無効な値を入力すると、フォームグループを揺らします(※ユーザーがモーション設定していない場合)。こういったマイクロインタラクションも:has()
疑似クラスを使用すると、簡単に実装できます。
See the Pen
This <input> :has an error message by web.dev (@web-dot-dev)
on CodePen.
コンテンツの実装
コンテンツの実装は:has()疑似クラスの基本的な使い方で触れましたが、ドキュメントフローで:has()
疑似クラスを使用するにはどうすればよいでしょうか?
たとえば、テキスト間にある画像のスタイルです。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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
がある場合は全幅を占め、追加のマージンを与えます。
実際の動作は、デモをご覧ください。
See the Pen
An article that :has <figure> by web.dev (@web-dot-dev)
on CodePen.
状態への反応
マークアップのある状態に対してスタイルを反応するというのはどうでしょうか。シンプルなスライド式のナビゲーションバーで見てましょう。
ナビゲーションを開くボタンがある場合はaria-expanded
属性を使用するかもしれません。JavaScriptで適切な属性に変更することができます。aria-expanded
がtrue
の場合は:has()
疑似クラスで検出し、ナビゲーションバーのスタイルを更新します。JavaScriptはその役割を果たし、CSSはその情報を使用して必要なことをすることができます。マークアップをシャッフルしたり、余分なクラス名を追加する必要はありません。
1 2 3 4 5 6 |
:root:has([aria-expanded="true"]) { --open: 1; } body { transform: translateX(calc(var(--open, 0) * -200px)); } |
実際の動作は、デモをご覧ください。
See the Pen
A site that :has a <nav> by web.dev (@web-dot-dev)
on CodePen.
※メニューにLight dismissを追加していません。これは実装する必要があります。Chrome Canaryでデモをチェックして、新しいpopup
属性がどのように役立つかを確認してください。
:has()疑似クラスは、ユーザーエラーを回避するのに役立つか
これらの例に共通するものは何でしょうか? :has()
疑似クラスの使用方法を示していることを除けば、クラス名を変更・追加する必要がないことです。それぞれが新しいコンテンツを挿入し、属性を更新しています。これはユーザーのミスを軽減するという意味で、:has()
疑似クラスの大きな利点です。
:has()
疑似クラスを使用すると、CSSはDOMの変更に対応する責任を負うことができます。JavaScriptでクラス名を変更する必要がないため、デベロッパーのミスも減らすことができます。クラス名をタイプミスして、オブジェクトのルックアップを保持しなければならなかったことは、誰もが経験したことです。
これは興味深い考え方で、よりクリーンなマークアップ、より少ないコードにつながるのでしょうか? JavaScriptはあまり使用していないので、JavaScriptは少なくなります。HTMLはcard card--has-media
のようなクラスが必要なくなるので、HTMLも少なくなります。
枠にとらわれない考え方
前述したように、:has()
疑似クラスはメンタルモデルを壊すことを促します。これは、いろいろなことに挑戦できるチャンスです。CSSだけでゲームの仕組みを作ることも、限界を超える方法の一つです。たとえば、フォームとCSSを使用してステップベースのメカニズムを作ることができます。
1 2 3 4 5 6 7 8 |
<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> |
1 2 3 4 5 6 7 8 9 10 |
.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; } |
実際の動作は、デモをご覧ください。
See the Pen
:has a game mechanic 🕹 by web.dev (@web-dot-dev)
on CodePen.
さらに、興味深い可能性を広げてくれます。
上記を利用して、フォームをtransform
で次々に水平移動、トラバースさせることもできます。下記のデモを見るときは、右上の「Edit on CodePen」をクリックして別タブで開くのがベストです。
See the Pen
:has transforming <form> items 🚀 by web.dev (@web-dot-dev)
on CodePen.
またゲームとして、イライラ棒はいかがですか?
仕組みは:has()
疑似クラスを使用すると簡単に作成できます。ワイヤーにカーソルが触れると、ゲームオーバーになります。+
と~
を組み合わせて作成することもできますが、:has()
疑似クラスを使用すればマークアップのトリックを使用せずに同じ結果を得ることができます。このデモも右上の「Edit on CodePen」をクリックして別タブで開くのがベストです。
See the Pen
Do you :has a steady hand? 👋 [Pure CSS Game] by web.dev (@web-dot-dev)
on CodePen.
これらをすぐに実運用することはないと思いますが、:has()
疑似クラスの可能性を探るにはよい方法です。ほかにも、:has()
疑似クラスをチェーンさせることもできます。
1 2 3 4 |
:root:has(#start:checked):has(.game__success:hover, .screen--win:hover) .screen--win { --display-win: 1; } |
この記事で紹介したデモは、:has()
疑似クラスによって可能になった興味深い事例を示したものです。プラットフォームの新機能は、特にインタラクションが関係するものは潜在的なアクセシビリティの問題の観点から慎重に評価する必要があります。
:has()疑似クラスのパフォーマンスと制限
その前に、:has()
疑似クラスでできないことは何でしょうか? :has()
疑似クラスにはいくつかの制約があります。主なものは、パフォーマンスへの影響です。
:has()
と:has()
の直結はできません。しかし、チェーンはできます。
1:has(.a:has(.b)) { … }:has()
内で疑似要素は使用できません。
12:has(::after) { … }:has(::first-letter) { … }- 合体選択子のみを受け入れる擬似要素・疑似クラス内で
:has()
は使用できません。
1234::slotted(:has(.a)) { … }:host(:has(.a)) { … }:host-context(:has(.a)) { … }::cue(:has(.a)) { … } - 疑似要素の後に
:has()
は使用できません。
1::part(foo):has(:focus) { … } :visited
の使用は常にfalseになります。
1:has(:visited) { … }
:has()
疑似クラスのパフォーマンスについては、:has() performance snapshotsをご覧ください。実装に関するこれらの洞察と詳細を共有してくれたByungwooに感謝します。
終わりに
:has()
疑似クラスの準備をしておきましょう。
この疑似クラスは、CSSへの取り組み方法を一変させます!
すべてのデモは、CodePenのコレクションで利用できます。
sponsors