【実務・中級編】 関係擬似クラス :has() と複雑なセレクタの組み合わせ – CSS実践ガイド

おい、調子はどうだ?
今日も元気に `!important` の海で溺れかけてないか?(笑)

まあ座れ。今日はな、CSSの歴史を塗り替えたと言っても過言ではない、あの最強にして最恐の機能……`:has()` 擬似クラスについて話をしよう。

これまで「親要素を選択できない」というCSSの長年の呪縛に、俺たちは何度涙を飲んできたことか。JavaScriptでわざわざ `MutationObserver` を回したり、クラスをバニラJSでパチパチ付け替えたり……そんな泥臭いハックとも今日でおさらいだ。

だがな、`:has()` が「CSS界のgetParent()」だと思って、なんでもかんでも適当にブチ込んでいないか?
「子孫結合子」や「隣接兄弟結合子」を組み合わせた途端に、ブラウザの描画エンジンが裏でどれだけの重労働を強いられているか、考えたことはあるか?

今日は中級から一歩抜け出して「本当の意味でパフォーマンスとメンテナンス性を両立できるプロの書き方」を、俺が現場のリアルな知見と共にお前たちに伝授しよう。耳の穴かっぽじってよく聞けよ。

—

1. `:has()` とは何か?そしてブラウザの裏側で何が起きているのか

まず基本の復習だ。`:has()` は一言で言えば「条件を満たす子孫や兄弟を持つ要素を、親の方向に向かって選択する(リレーショナル・セレクタ)」ためのものだ。

例えば、`div:has(img)` なら「中に `img` を持っている `div`」を指定できる。これだけでも革命的だったが、本領を発揮するのはここからだ。子孫結合子(スペース)や隣接兄弟結合子(`+`)、一般兄弟結合子(`~`)と組み合わせた瞬間、`:has()` は単なる「親セレクタ」を超えて、「DOMの構造的状態マシン」に変貌する。

ブラウザのレンダリングの裏側はどうなっている?

ここで、シニアとしてお前らに一番伝えておきたい「ブラウザの裏側の話」をしておこう。

CSSセレクタは、基本的に右から左(Right-to-Left)に評価される。`div p span` なら、まずすべての `span` を探し、その親が `p` か、さらにその親が `div` かを遡る。

では、`:has()` が入るとどうなるか?
ブラウザはDOMツリーを構築する際、`:has()` の中身(引数)に合致する要素が変化したとき、あるいはツリーが構築されたタイミングで、親方向への再評価(Style Recalculation)を走らせる。
つまり、複雑な結合子(`.card:has(> .header + .content:has(input:focus))` など)を無秩序に書くと、DOMの小さな変化がツリー全体の広範囲な再計算を引き起こし、ガクガクのスクロールや入力遅延(Jank)の元凶になる。

だからこそ、俺たちは「ただ動くコード」ではなく、「ブラウザに優しい、計算量の少ないセレクタの組み方」を知る必要があるんだ。

—

2. 実務で即効性のある実践的デザインパターン 3選

百聞は一見に如かずだ。現場で「これ、欲しかったんだよ!」と叫びたくなるような具体的なユースケースを3つ、美しいコードと共に見せてやろう。

パターンA:子要素の「数」や「有無」に応じたカードの動的レイアウト

ECサイトのプロダクトカードやブログのグリッドを想像してくれ。画像があるかないかでレイアウトを切り替えたい、あるいは「バッジ」がいくつ入っているかで見た目を変えたい。そんな時、従来ならJSでクラスを付与していたはずだ。

これを `:has()` と子孫結合子でスマートに解決する。

/ ==========================================
パターンA: 画像の有無とステータスによるカードの変形
================================———- /

.product-card {
display: grid;
grid-template-rows: auto 1fr auto;
padding: 1.5rem;
border: 1px solid #e2e8f0;
border-radius: 12px;
background: #ffffff;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}

/ 1. 画像(.thumb)を持っているカードだけ、レイアウトとパディングを調整する /
.product-card:has(.thumb) {
grid-template-rows: 200px 1fr auto;
padding: 0; / 画像を端までbleedさせるためにパディングを消す /
overflow: hidden;
}

/ 画像コンテナの基本スタイル /
.product-card .thumb {
width: 100%;
height: 100%;
object-fit: cover;
}

/ 2. さらに「セール中バッジ(.badge-sale)」かつ「SOLD OUT」ではない複雑な条件 /
.product-card:has(.badge-sale):not(:has(.sold-out)) {
border-color: #f43f5e;
box-shadow: 0 4px 20px rgba(244, 63, 94, 0.15);
}

どうだ? `.product-card:has(.thumb)` とするだけで、画像がある場合とない場合のカードの構造変化をCSSだけで綺麗にカプセル化できる。HTML側に `is-with-image` みたいなどうでもいいクラスを量産しなくてよくなるんだ。

—

パターンB:フォームのバリデーションと隣接・子孫結合子のコンビネーション

お次はフォームだ。入力エラーが起きたとき、その入力フィールドだけでなく、「エラーを抱えているグループ全体(フォームグループ)」の枠線を赤くし、さらにエラーメッセージをフワッと出したい。

ここで隣接兄弟結合子(`+`)や子孫結合子の出番だ。

/ ==========================================
パターンB: フォームの入力状態に応じたインタラクティブな装飾
================================———- /

.form-group {
position: relative;
margin-bottom: 1.5rem;
padding: 1rem;
border: 2px solid #cbd5e1;
border-radius: 8px;
background-color: #f8fafc;
transition: border-color 0.3s, background-color 0.3s;
}

/ 内部のinputがフォーカスされている場合、親のgroupをハイライト /
.form-group:has(input:focus) {
border-color: #3b82f6;
background-color: #ffffff;
}

/ 内部のinputが「入力必須かつ未入力で、かつバリデーションが発火している(またはinvalid)」状態 /
/ ここで子孫結合子と疑似クラスを組み合わせる /
.form-group:has(input:user-invalid) {
border-color: #ef4444;
background-color: #fef2f2;
}

/ エラー時のラベルやメッセージの制御 /
.form-group .error-message {
display: none; / デフォルトは非表示 /
font-size: 0.875rem;
color: #ef4444;
margin-top: 0.5rem;
}

/ 「エラー状態のinput」の直後にあるエラーメッセージを表示する(隣接兄弟結合子との組み合わせ) /
.form-group:has(input:user-invalid) .error-message {
display: block;
animation: fadeIn 0.2s ease-in-out;
}

@keyframes fadeIn {
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: translateY(0); }
}

このコードの何が美しいかって?
JSで `element.classList.add(‘is-error’)` をチマチマ書く必要が一切ない点だ。ブラウザがネイティブのフォーム状態(`:user-invalid` など)を監視し、`:has()` がそれを検知して親を一発でスタイリングする。モダンブラウザの恩恵を最大限に受けた、極めてロバストな設計だ。

—

パターンC:テーブルの行(tr)とセル(td)の高度なホバーエフェクト

データビジュアライゼーションや管理画面のダッシュボードでよくある要件だ。「表の特定のセルにマウスオーバーしたとき、その行全体だけでなく、同じ列(縦方向)もうっすらハイライトしたい」と思ったことはないか?

実は、`:has()` を使うと、純粋なCSSだけで「特定の列ホバー」の雰囲気をグッと出しやすくなる。(※完全な列選択は `:nth-child()` の組み合わせが必要だが、特定のインタラクションにおいて爆発的な効果を発揮する)

今回は、「エラーが含まれている行(tr)」を丸ごと警告色に変えつつ、さらにその中で特定の重要フラグ(`.is-critical`)を持つセルが存在する場合に、行全体の背景色を点滅させるような、少し高度な例を見せてやろう。

/ ==========================================
パターンC: テーブル行(tr)の条件付きハイライト
================================———- /

.data-table {
width: 100%;
border-collapse: collapse;
}

.data-table th,
.data-table td {
padding: 12px 16px;
border-bottom: 1px solid #e2e8f0;
text-align: left;
}

/ 基本の行ホバー /
.data-table tr:hover {
background-color: #f1f5f9;
}

/ 1. 子孫に「ステータスエラー(.status-error)」を持つtr行全体をスタイリング /
.data-table tr:has(.status-error) {
background-color: #fff1f2;
}

/ 2. さらにその行の中に「緊急フラグ(.is-critical)」を持つ要素が含まれている場合 /
.data-table tr:has(.status-error):has(.is-critical) {
background-color: #ffe4e6;
border-left: 4px solid #e11d48;
}

/ 3. チェックボックスが選択されている行のスタイル(いわゆる行選択状態) /
.data-table tr:has(input[type=”checkbox”]:checked) {
background-color: #eff6ff;
}

「おっ、これなら管理画面のUI実装が一気に楽になるな」と思ったはずだ。そう、複雑な状態管理をCSS側にオフロードできるのが、`:has()` の真骨頂なんだよ。

—

3. シニアが教える「現場で絶対に守るべき鉄則とアンチパターン」

さて、ここまで持ち上げておいて何だが、最後に冷水を浴びせるような話をしよう。
強力な機能には、必ず相応のリスクが伴う。現場で `:has()` を使うときに、俺たちがチームメンバーに厳しくレビューしている「罠」をいくつか共有しておく。

1. 深すぎるネスト(Deep Nesting)はパフォーマンスの毒薬

先ほども言ったが、`:has()` の中にさらに `:has()` を入れ子にしたり、巨大なDOMツリーのルート近くで複雑な子孫結合子を使ったりすると、ブラウザの再描画コストが跳ね上がる。

  • NG例: `body:has(.sidebar:has(.menu:has(li:has(a.active))))`

(こんなの書いたやつは、俺がコードレビューで容赦なく差し戻す)

  • OK例: 影響範囲をコンポーネント単位(BEMのブロック単位など)に閉じ込め、せいぜい1〜2階層の深さにとどめる。

2. JavaScriptによる状態管理との役割分担をきっちり見極めろ

「なんでもかんでもCSSだけでやろうとする病」に罹るジュニアがたまにいる。
アニメーションのトリガーや、複雑なトグル状態、APIのフェッチ状態などは、やはりJavaScriptの責務だ。CSSの `:has()` はあくまで「DOMの構造や属性の変化に連動した、表示のスタイリング(プレゼンテーション)」に絞るべきだ。ロジックをCSSに持ち込みすぎると、かえってデバッグが地獄のように困難になる。

3. フォールバック(環境依存)の考慮

現在のモダンブラウザ(Chrome, Safari, Firefox, Edge)の主要バージョンではすべて標準サポートされているため、実務での導入障壁はほぼ消滅した。だが、万が一レガシー環境を考慮しなければならないプロジェクトであれば、`@supports (selector(:has()))` を使うか、従来通りJSでクラスを付与するフォールバックを設計の初期段階で仕込んでおくことだ。プロなら「動かない環境」への備えを忘れない。

—

まとめ

`:has()` と複雑な結合子の組み合わせは、フロントエンド開発のゲームチェンジャーだ。
これまでJSのDOM操作や複雑なクラス管理に頼っていた処理を、宣言的なCSSの記述だけでエレガントに解決できる。

だが、忘れるな。「力には責任が伴う」。
ブラウザのレンダリングメカニズムを正しく理解し、パフォーマンスに配慮した簡潔なセレクタを書くことこそが、中級から「真のシニア・フロントエンドエンジニア」へとステップアップするための必須条件だ。

さあ、理屈はここまでだ。
今書いてるそのコードの無駄なJavaScriptを削ぎ落として、美しい `:has()` に書き換えてこい。何か詰まったら、いつでも俺のところへ聞きに来いよ。健闘を祈る!

コメント

タイトルとURLをコピーしました