CSSの歴史において、長年の悲願であった「親セレクタ」がついに実装され、私たちのワークフローに革命が起きてからしばらく経ちます。そう、` :has()` 疑似クラスのことです。
「あ、これ親要素をスタイルしたいのに、CSSだけじゃ無理だからJavaScriptでクラスを付与するか……」
フロントエンドの実務で、そんな悔しい妥協をした経験は誰にでもありますよね。DOMの構造上、子から親へはスタイルが伝搬しないというCSSの長年の鉄則があったからです。しかし、`:has()` の登場により、その常識は完全に覆されました。
今回は、中級から一歩抜け出して「真のCSSアーキテクト」を目指す君たちに向けて、この `:has()` 関係疑似クラスの実務での実践知を徹底的に叩き込みます。ただの使い方のお勉強ではなく、ブラウザの裏側の挙動やパフォーマンスへの配慮まで含めた、現場で即戦力になる知識を授けましょう。
—
1. `:has()` とは何か?(基本思想の再確認)
一言で言えば、`:has()` は「条件に一致する子孫や兄弟を持つ、手前の要素(親や先行要素)を選択する」ためのものです。
従来のCSSセレクタは、右から左(DOMツリーの根から葉の方向)へ流れるように評価されていました。「`div` の中にある `p`」という指定は、「`div` を見つけて、その中にある `p` を探す」という動きです。
しかし、`:has()` は逆のベクトルを持ちます。「特定の要素を持っているか?」を判定基準にして、その起因となった要素をスタイリングします。まさに、CSSにおける「条件付き逆引きセレクタ」です。
2. ブラウザの裏側で何が起きているか?(パフォーマンスの罠)
さて、ここからがシニアとしての重要なアドバイスです。「便利だからといって、何でもかんでも `:has()` を書けばいい」というわけではありません。
ブラウザのレンダリングエンジン(Blink, Gecko, WebKitなど)の裏側の話をしましょう。
通常、CSSのセレクタマッチングは非常に高速に最適化されています。しかし、`:has()` はDOMの構造を上下に走査(トラバーサル)する必要があるため、使い方を誤ると「パフォーマンスキラー」になり得ます。
- グローバルな `:has()` は重い:
例えば、`body:has(input:focus)` のような指定は、ページ内のどこかの `input` がフォーカスされるたびに、ブラウザは `body` 全体のツリーを再評価することになります。これが大規模なDOMツリーであれば、わずかに入力が遅れる(カクつく)原因になります。
- スコープを狭める意識:
極力、影響範囲を限定したコンポーネント単位で `:has()` を使うのが、実務におけるベストプラクティスです。「コンポーネントのルート」や「カード単位」といった局所的なスコープであれば、ブラウザの再計算コストは最小限に抑えられます。
3. 現場で即コピペして使える!実践的ユースケース4選
理屈はこのあたりにして、実際のプロジェクトで明日から使える具体的なコードを見ていきましょう。どれも「これ、JSでやってたわ……」と膝を打つようなユースケースです。
ケースA:子要素の有無でレイアウトを劇的に変える「空っぽの恐怖」からの解放
CMSから出力されるリッチテキストやカードコンポーネントで、「画像がある時は2カラム、画像がない時は1カラム(テキスト全幅)」にしたい要件はよくあります。従来ならJSでクラスをつけていましたよね。
/ カードの中に画像(.card__image)が存在する場合のみ、グリッドを2カラムにする /
.card:has(.card__image) {
display: grid;
grid-template-columns: 240px 1fr;
gap: 1.5rem;
}
/ 画像がないカードはデフォルトの1カラムを維持する自然なフォールバック /
.card {
display: block;
padding: 1.5rem;
background-color: #ffffff;
border-radius: 8px;
}
ケースB:フォームの「入力完了・エラー・フォーカス」状態を親ゴトデザインする
フォームのバリデーションやインタラクティブなUIにおいて、入力欄(input)の状態に応じて、その外側のコンテナ(ラベルやボーダー、背景色など)全体をハイライトしたいときは `:has()` の独壇場です。
/ 入力欄(input)にフォーカスが当たっている時、親であるフォームグループ全体をアクティブ化 /
.form-group:has(input:focus) {
border-color: #0066cc;
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.15);
}
/ エラー状態のinputを含んでいる場合、グループ全体の枠線を赤くし、警告アイコンの表示を制御する /
.form-group:has(input:invalid:not(:placeholder-shown)) {
border-color: #dc3545;
}
.form-group:has(input:invalid:not(:placeholder-shown)) .error-message {
display: block; / エラーメッセージを表示 /
}
プロの技: `:not(:placeholder-shown)` を組み合わせることで、「最初から空欄のエラー」を初期表示時に防ぐテクニックは、実務で必須のイディオムです。
ケースC:チェックされたら行全体(またはカード全体)の色を変える
テーブルの行や、チェックボックス付きのカードUIで、「選択されたら背景色を変える」という要件も非常にスマートに書けます。
/ チェックボックスがチェックされている状態の子孫を持つ、リストアイテムを選択 /
.todo-item:has(input[type=”checkbox”]:checked) {
background-color: #f8f9fa;
opacity: 0.7;
}
/ チェックされていたら中のテキストに取り消し線を引く /
.todo-item:has(input[type=”checkbox”]:checked) .todo-text {
text-decoration: line-through;
color: #6c757d;
}
JavaScriptのイベントリスナーを書いて `classList.toggle(‘is-checked’)` なんて記述していた日々とは、これでサヨナラです。
ケースD:兄弟セレクタの進化系!「直後の要素によって自分のスタイルを変える」
`:has()` は子孫だけでなく、兄弟要素(Combinator)とも組み合わせられます。これがまた強力です。
/ 見出し(h2)の直後に特定のアラートバナー(.alert)が続く場合、見出しの下部マージンを詰める /
h2:has(+ .alert) {
margin-bottom: 0.5rem;
}
.alert {
margin-bottom: 1.5rem;
}
「直前の要素を見る」ことはCSSの構造上不可能でしたが、後ろの要素を条件に前の要素をスタイリングできるようになったことで、タイポグラフィの微調整が劇的にエレガントになりました。
—
4. 現場での導入における注意点(まとめに代えて)
最後に、実務で `:has()` を採用する際の心構えを伝えておきます。
1. モダンブラウザのサポート状況はほぼクリア:
主要なモダンブラウザ(Chrome, Safari, Firefox, Edge)の現行バージョンでは完全にサポートされています。レガシーブラウザ(IE11など)の対応が完全に終わっているプロジェクトであれば、恐れずに導入して構いません。
2. 保守性を意識したコメントを残す:
「親セレクタ」という概念にまだ慣れていないメンバーがチームにいるかもしれません。「なぜここで `:has()` を使っているのか」がひと目で分かるように、今回紹介したような丁寧なコメントをCSSに残す配慮を忘れないでください。
CSSは単なる「見た目を飾る言語」から、「DOMの構造と状態をエレガントに制御するプログラミングに近い表現力を持つ言語」へと進化しました。`:has()` を使いこなし、JSに頼らない軽快で美しいフロントエンドアーキテクチャを構築していきましょう。君ならできるはずです。

コメント