CSS :has()の使い方|親要素を選択できる注目の疑似クラスを解説

ソースコードが書かれているモニター

「画像が入っているときだけ、この枠の見た目を変えたい」。CSSを書いていて、そんな場面に一度は出くわしたことはありませんか。

ところが従来のCSSは、子や孫の要素は選べても、その「親」を選ぶことはできませんでした。私もWeb制作を25年やってきて、この一点だけはずっと歯がゆく思っていました。仕方なくJavaScriptでクラスを付け外ししたり、HTMLの構造そのものを変えたり……。地味に面倒な回り道です。

たとえば「画像つきの記事カードだけ余白を変えたい」「入力が終わったフォームの行だけ色を変えたい」。どれもありがちな要望なのに、CSSだけでは届かない。たったそれだけのためにJavaScriptを引っぱり出すのは、正直やりすぎだとずっと感じていました。

それを一発で解決してくれるのが、今回紹介する:has()という疑似クラスです。:has()は「特定の要素を中に持つ親」を選べるCSSの機能で、2023年末にすべての主要ブラウザで使えるようになった、いま最も注目すべきセレクタの一つです。

この記事では、:has()の基本の書き方から、実際の制作で効く活用例、つまずきやすい注意点までを、初心者の方にもわかるようにかみ砕いて解説します。コード例はそのままコピーして試せる形で載せました。読み終えるころには、「あの面倒な処理、:has()で済むじゃないか」と、使えるテクニックが一つ増えているはずです。

:has()ってなに? ひとことで言えば「親を選べるCSS」

まず、これまでのCSSの大前提をおさらいします。CSSは基本的に「外から内へ」スタイルが流れる仕組みです。親から子を指定するのは得意ですが、その逆——子の状態を見て親を選ぶ——は長らくできませんでした。

たとえるなら、中身を見てから箱を選びたいのに、箱の外側からしか触れないもどかしさ。これがWeb制作者を地味に悩ませてきました。

:has()は、この壁をあっさり壊します。カッコの中に書いた要素を「含んでいる」要素を選べる疑似クラスで、つまり中身の状況に応じて、その親や前の兄弟要素にスタイルを当てられるようになったのです。

海外では「the parent selector(親セレクタ)」という愛称で呼ばれることもあります。長年「CSSにはできない代表例」とされてきた機能だけに、登場したときは界隈がちょっとざわつきました。

参考: MDN Web Docs

基本の書き方は「親:has(中の要素)」

構文はとてもシンプルです。スタイルを当てたい親要素のうしろに:has()を付けて、カッコの中に「中にある条件」を書くだけ。

/* 画像を含む section の枠線を変える */
section:has(img) {
  border: 2px solid #ff7a59;
}

この数行で、中に<img>が入っているsectionだけを狙い撃ちできます。画像のないsectionには何も起こりません。

前の兄弟要素も選べる

結合子と組み合わせると、もっと面白くなります。たとえば「直後にh2が続くh1」を選ぶならこう書きます。

/* h1 の直後に h2 があるときだけ余白を詰める */
h1:has(+ h2) {
  margin-bottom: 0.25rem;
}

見出しが連続したときの間隔を、HTMLを触らずに整えられます。地味ですが、こういう「あと一歩の調整」がCSSだけで完結するのは気持ちいいものです。

「AND」と「OR」も自由自在

条件は重ねられます。書き方で意味が変わるので、ここだけ覚えておくと便利です。

/* video と audio の両方を含む(AND) */
body:has(video):has(audio) { ... }

/* video か audio のどちらかを含む(OR) */
body:has(video, audio) { ... }

:has()を続けて書けばAND、カンマで区切ればOR。理屈がわかれば、組み合わせは一気に広がります。

関連記事:【CSS】横並びメニューの区切り線を「隣接セレクタ」と「疑似要素」を使って実装する方法

制作現場で「これは効く」活用例

中身のあるカードだけ装飾する

記事一覧でよくある「サムネイル付きカード」。画像がある記事とない記事で見た目を変えたいとき、これまではテンプレート側で条件分岐を書きがちでした。:has()ならCSS一発です。

/* 画像があるカードだけ上の余白を消す */
.card:has(img) {
  padding-top: 0;
}

入力済みのフォームを目立たせる

フォームのチェックボックスがオンのときだけ、その行を強調する——といった「状態に反応する見た目」もお手のもの。

li:has(input:checked) {
  background: #fff6e5;
}

JavaScriptでイベントを監視しなくても、CSSだけで状態に反応する表現ができるのが:has()の真骨頂です。私もフォームまわりの実装で、コード量がぐっと減りました。

エラーのある項目だけ赤くする

もう一つ、私がよく使うのが入力エラーの表現です。:invalidなどと組み合わせれば、不正な値が入っている入力欄を含む行だけを、まとめて赤系の見た目に切り替えられます。

/* 不正な入力を含む行を赤くする */
.form-row:has(input:invalid) {
  border-left: 3px solid #e53935;
}

「どこを直せばいいの?」と迷わせない親切な画面が、CSSだけで作れます。ユーザー体験に直結する部分なので、覚えておいて損はありません。

使う前に知っておきたい3つの注意点

1. :has()の中に:has()は入れられない

入れ子(ネスト)はできません。また::beforeのような疑似要素を:has()の中の条件に使うこともできない決まりです。最初に知っておくと、ハマらずに済みます。

2. 範囲を広げすぎるとページが重くなることも

便利だからとbody:has(...)*:has(...)のように対象を広く取ると、ブラウザが大量の要素をチェックすることになり、表示が重くなる場合があります。

できるだけ.cardのような具体的な親に絞り、>で直接の子だけを見るようにすると安心です。

/* 重くなりがちな書き方 */
body:has(.menu-open) { ... }

/* 範囲を絞った書き方 */
.header:has(> .menu-open) { ... }

3. 非対応ブラウザでは効かない

ごく古いブラウザでは:has()そのものが無効になり、場合によってはそのCSSルール全体が読み飛ばされることがあります。レイアウトの土台ではなく「あるとうれしい装飾」に使うと、もしものときも崩れにくくなります。

参考: MDN Web Docs(:has())

ブラウザ対応は、もう心配いりません

「新しい機能」と聞くと対応状況が気になりますよね。結論から言うと、:has()はすでに安心して使えるレベルに達しています。

最後まで未対応だったFirefoxが2023年12月のバージョン121で:has()に対応し、これで主要ブラウザがすべて出そろいました。同じ2023年12月には、Webの互換性の指標「Baseline」でも「newly available(広く使える状態になった)」と位置づけられています。

つまり、最新版のChrome・Edge・Safari・Firefoxを使っている人なら、まず問題なく表示されるということ。登場から年月も経ち、実務で使う土台は十分に整いました。

参考: Firefox 121 リリースノート

まとめ

長年「CSSにはできない」とされてきた親要素の選択。それを当たり前にしてくれたのが:has()です。

やることは親:has(中の条件)と書くだけ。中に持っている要素や、直後に続く要素を手がかりに、これまで手の届かなかった親や兄弟へスタイルを当てられます。画像のあるカードだけ装飾したり、入力済みのフォームを光らせたり——いままでJavaScriptやテンプレートの分岐に頼っていた処理が、CSSの一行で片付くのは本当に快感です。

注意点は、ネストできないこと、範囲を広げすぎると重くなりうること、そしてごく古い環境では効かないこと。この3つさえ押さえれば、こわがる理由はありません。とくに「装飾やひと手間の調整から使い始める」と決めておくと、安全に慣れていけます。

振り返ってみると、私がWeb制作を始めたころは、ちょっとした見た目の出し分けにもJavaScriptや画像を持ち出すのが当たり前でした。それがCSSの一行で済む時代になったのですから、便利になったものです。新しい機能はつい身構えてしまいますが、:has()は構文がやさしく、すぐ手になじみます。

:has()は「親を選べる」というだけで、CSSの設計そのものをシンプルにしてくれる、知っておいて損のない武器です。

まずは手元のサイトで、画像のあるカードに一行足してみてください。「あ、これだけでいいのか」と、世界が少し変わるはずです。次にCSSを書くとき、選べる手が一つ増えています。