Back

CSS :has()セレクタ完全攻略:20年越しで実現した親セレクタ

Web開発者が20年以上待ち望んでいた機能がついに登場しました。CSSの親セレクタです。

以前は、子要素の状態に応じて親のスタイルを変えたい場合、JavaScriptを使うしかありませんでした。クラスをトグルしたり、DOM変更を監視したり...正直面倒でしたよね。でも今は:has()だけでできてしまいます。

実は:has()は「親セレクタ」と呼ぶにはもったいないくらい多機能です。兄弟関係、子孫の状態、さらには「次に何が来るか」まで条件にできます。まさにゲームチェンジャーですね。

この記事では:has()を徹底的に解説します。基礎から実践パターン、パフォーマンス最適化まで全部カバーします。読み終わる頃には、あなたのコードからJavaScript依存がかなり減っているはずです。

なぜ今まで実装されなかったのか?

ちょっと疑問に思いませんか?なぜ親セレクタがこんなに遅れたのでしょう。

従来の制限

CSSセレクタは本来、親→子の方向でしか動きません。

/* ✅ これはOK - 親を基準に子を選択 */ .card .title { font-size: 1.5rem; } /* ❌ これはNG - 子を基準に親を選択 */ /* .card:contains(.featured-badge) { ... } ← 以前は不可能 */

そのため開発者はこんな回避策を使っていました:

  1. クラスを手動で追加: .card.has-image { ... } のように
  2. JavaScriptで対応: DOM変更のたびにクラスをトグル
  3. HTML構造の変更: CSSの制限に合わせてマークアップを修正

どれも面倒で、メンテナンスも大変でした。

パフォーマンス問題が原因

ブラウザにとって親セレクタは厄介者でした。CSSセレクタは効率のため右から左へ評価されます。親セレクタを実装するには:

  1. 全ての子孫要素を探索
  2. それぞれでDOMツリーを遡る
  3. 祖先にスタイルを適用

複雑なページではレンダリングが著しく遅くなる可能性がありました。幸い:has()の仕様はこれを効率的に処理できるよう設計されており、実務で問題なく使えます。

:has()の基本

:has()の中にセレクタを入れると、そのセレクタにマッチする子孫を持つ要素が選択されます。

基本構文

:has(子セレクタ) { /* 親に適用されるスタイル */ }

括弧内の条件を満たす子孫があれば、親が選択される仕組みです。

最初の例

カードコンポーネントで試してみましょう。

<div class="card"> <img src="product.jpg" alt="商品"> <h3>商品名</h3> <p>説明文です...</p> </div> <div class="card"> <h3>画像なしカード</h3> <p>テキストのみです。</p> </div>

以前なら画像の有無でスタイルを変えるには、クラスを追加するかJavaScriptが必要でした。今は:

/* 基本スタイル */ .card { padding: 1rem; border-radius: 8px; background: white; } /* 画像があればグリッドに */ .card:has(img) { display: grid; grid-template-columns: 200px 1fr; gap: 1rem; } /* 画像がなければ中央揃え */ .card:not(:has(img)) { text-align: center; max-width: 400px; }

JavaScript一行もなしで、コンテンツに応じて自動的にレイアウトが変わります。

親セレクタ以上の実力

「親セレクタ」という呼び方だけでは:has()の能力を表しきれません。

兄弟要素を基準に選択

+コンビネータと組み合わせれば兄弟関係も条件にできます。

/* 直後にrequiredなinputがあるlabel */ label:has(+ input:required) { font-weight: bold; } label:has(+ input:required)::after { content: " *"; color: #e74c3c; }

フォームのスタイリングでかなり重宝します。

深い階層まで探索

:has()内のセレクタは子孫全体を探索します。

/* どこかにエラーメッセージがあればセクションを強調 */ .form-section:has(.error-message) { border-left: 4px solid #e74c3c; background: #fdf2f2; } /* 編集可能なセルがある行を強調 */ tr:has(td[contenteditable="true"]) { background: #fffef0; }

条件の組み合わせ

:has()を複数繋げてAND条件に:

/* 画像とバッジの両方があるカード */ .card:has(img):has(.featured-badge) { box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15); border: 2px solid gold; }

OR条件は括弧内にカンマで:

/* 動画か画像があれば */ .card:has(img, video) { min-height: 300px; }

実践的なユースケース

理論はここまでにして、実際の活用法を見ていきましょう。

1. フォームバリデーションUI

:has()の真骨頂です。JavaScriptなしでリアルタイムバリデーションが可能に:

/* 無効な入力がある場合 */ .form-group:has(input:invalid:not(:placeholder-shown)) { --input-border-color: #e74c3c; --input-bg: #fdf2f2; } /* 有効な入力の場合 */ .form-group:has(input:valid:not(:placeholder-shown)) { --input-border-color: #27ae60; --input-bg: #f0fdf4; } .form-group input { border: 2px solid var(--input-border-color, #ddd); background: var(--input-bg, white); transition: all 0.2s ease; } /* エラーメッセージは必要な時だけ表示 */ .form-group .error-message { display: none; color: #e74c3c; font-size: 0.875rem; } .form-group:has(input:invalid:not(:placeholder-shown)) .error-message { display: block; }

本当にJavaScript一行も必要ありません。

2. ナビゲーションのアクティブ状態

子メニューがアクティブなら親も強調したい場合:

/* 子リンクがactiveならドロップダウンボタンも強調 */ .nav-dropdown:has(.nav-link.active) > .dropdown-toggle { color: var(--primary-color); font-weight: bold; } /* サブメニューがあれば矢印表示 */ .nav-item:has(.submenu)::after { content: "▼"; font-size: 0.75em; margin-left: 0.5rem; }

3. 空状態(Empty State)の表示

コンテナにアイテムがない時、自動で案内を表示:

.item-grid:not(:has(.item)) { display: flex; align-items: center; justify-content: center; min-height: 300px; } .item-grid:not(:has(.item))::before { content: "表示する項目がありません"; color: #999; font-style: italic; }

4. アイテム数に応じたレイアウト

ちょっと面白い使い方です。子要素の数でスタイルを変えられます:

/* 1つだけなら全幅 */ .item:only-child { width: 100%; } /* ちょうど2つなら半分ずつ */ .item:first-child:nth-last-child(2), .item:last-child:nth-last-child(2) { width: 50%; } /* 4つ以上ならグリッドに */ .item-container:has(.item:nth-child(4)) { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); }

5. キーボードフォーカス管理

アクセシビリティ向上にも有効です。

/* カード内要素にフォーカスがあればカード全体を強調 */ .card:has(:focus-visible) { outline: 3px solid var(--focus-color); outline-offset: 2px; } /* フォーカスされたカード以外は少し暗く */ .card-container:has(.card:focus-visible) .card:not(:focus-visible) { opacity: 0.7; }

6. コンテンツベースのレスポンシブ

ヘッダーに検索バーがあるかないかでレイアウトを変えるような場合:

.header:has(.search-bar):has(.user-menu) { display: grid; grid-template-columns: auto 1fr auto; } .header:has(.search-bar):not(:has(.user-menu)) { display: grid; grid-template-columns: auto 1fr; }

7. テーブルの動的スタイリング

/* チェックされた行を強調 */ tr:has(input[type="checkbox"]:checked) { background: #e3f2fd; } /* ソート可能なテーブルならヘッダーにカーソル変更 */ table:has(th[data-sortable]) thead { cursor: pointer; } /* 空テーブルの表示 */ table:not(:has(tbody tr))::after { content: "データがありません"; display: block; text-align: center; padding: 2rem; color: #999; }

上級テクニック

:has()と:not()の組み合わせ

このコンボが非常に強力です。

/* 広告がないコンテナだけパディング追加 */ .container:not(:has(.advertisement)) { padding: 2rem; } /* メディアなしのテキストのみカード */ .card:not(:has(img, video, iframe)) { font-size: 1.1rem; line-height: 1.8; }

開発時のデバッグ活用

これは本当に便利な小技です。

/* altがない画像を見つける */ img:not([alt]), img[alt=""] { outline: 5px solid red !important; } /* actionがないformを見つける */ form:not([action]) { outline: 3px dashed orange !important; } /* セキュリティ上問題のあるリンク */ a[target="_blank"]:not([rel*="noopener"]) { outline: 3px solid purple !important; }

開発モードでのみ適用すれば、アクセシビリティやセキュリティの問題を一目で発見できます。

「前の兄弟」選択トリック

CSSには「前の兄弟セレクタ」がありませんが、:has()で似たことができます。

/* activeアイテムの直前のアイテム */ .item:has(+ .item.active) { border-right: none; } /* 特別なコンテンツの前にある見出し */ h2:has(+ .special-content) { color: var(--accent-color); border-bottom: 3px solid currentColor; }

パフォーマンスは大丈夫?

当然気になりますよね。「親セレクタはパフォーマンス問題で見送られたはずでは?」

モダンブラウザの最適化

  1. 探索深度の制限: 無限に深く探索しない
  2. 結果のキャッシュ: 一度計算したら保存
  3. DOM変更の一括処理: 毎回再計算せずまとめて処理

それでも守るべきこと

具体的なセレクタを使う

/* ❌ 範囲が広すぎる */ div:has(img) { ... } /* ✅ 範囲を限定 */ .card:has(img) { ... }

深いネストを避ける

/* ❌ 深すぎ */ .page:has(.section .container .row .col .card .badge) { ... } /* ✅ 直接関係に限定 */ .card:has(> .badge) { ... }

直接子コンビネータの活用

/* ❌ 全子孫を探索 */ .menu:has(.active) { ... } /* ✅ 直接の子だけ確認 */ .menu:has(> .menu-item.active) { ... }

ブラウザサポート状況

2024年末時点でほぼ全て対応しています。

  • Chrome: 105+ (2022年8月)
  • Edge: 105+ (2022年8月)
  • Safari: 15.4+ (2022年3月)
  • Firefox: 121+ (2023年12月)
  • Opera: 91+

世界のユーザーの95%以上:has()を使えます。プロダクションで使ってOKです。

念のためフォールバック

レガシーブラウザ対応が必要なら:

/* 基本スタイル */ .card { display: block; } /* :has()対応なら上書き */ @supports selector(:has(*)) { .card:has(img) { display: grid; grid-template-columns: 200px 1fr; } }

よくある間違い

間違い1: 詳細度(Specificity)の勘違い

よくある誤解ですが、:has()は詳細度に影響しないと思われがちです。実際には、:has()は引数内の最も詳細度が高いセレクタと同じ詳細度を持ちます

/* 詳細度の計算 */ .card { ... } /* (0, 1, 0) */ .card:has(img) { ... } /* (0, 1, 1) -> imgが(0, 0, 1)なので加算される */ .card:has(#hero) { ... } /* (1, 1, 0) -> #heroが(1, 0, 0)なので跳ね上がる */

:where()は常に詳細度0ですが、:has()は中身によって詳細度が変わる点に注意してください。

/* 自然に詳細度が高くなるので、上書き時は注意 */ .card:has(img) { ... }

間違い2: 何でも:has()で解決しようとする

シンプルなものはクラスで:

/* ❌ 過度に複雑 */ .btn:has(svg):has(span) { ... } /* ✅ クラス一つで解決 */ .btn.icon-button { ... }

間違い3: 全体セレクタに:has()

/* ❌ これは遅い */ *:has(.something) { ... } /* ✅ 範囲を絞る */ .component:has(.something) { ... }

CSSネスティングとの組み合わせ

最近のネイティブCSSネスティングと組み合わせると、コードがかなりスッキリします。

.card { padding: 1rem; &:has(img) { display: grid; grid-template-columns: 200px 1fr; & img { border-radius: 8px; } } &:has(.badge) { position: relative; & .badge { position: absolute; top: -10px; right: -10px; } } }

実践リファクタリング例

ナビゲーションコンポーネントをリファクタリングしてみましょう。

Before: JavaScriptが必要だった頃

<nav class="main-nav"> <ul class="nav-list"> <li class="nav-item has-submenu expanded"> <a href="#">製品</a> <ul class="submenu"> <li><a href="#" class="active">ソフトウェア</a></li> <li><a href="#">ハードウェア</a></li> </ul> </li> </ul> </nav>
// こんなコードが必要だった document.querySelectorAll('.nav-item').forEach(item => { if (item.querySelector('.submenu')) { item.classList.add('has-submenu'); } if (item.querySelector('.active')) { item.classList.add('expanded'); } });

After: 純粋CSSで解決

<nav class="main-nav"> <ul class="nav-list"> <li class="nav-item"> <a href="#">製品</a> <ul class="submenu"> <li><a href="#" class="active">ソフトウェア</a></li> <li><a href="#">ハードウェア</a></li> </ul> </li> </ul> </nav>
/* JavaScript不要! */ .nav-item:has(.submenu) > a::after { content: "▼"; } .nav-item:has(.active) > .submenu { display: block; } .nav-item:has(.active) > a { font-weight: bold; color: var(--primary); }

結果:

  • ✅ HTMLがスッキリ(状態クラス不要)
  • ✅ JavaScript依存の削除
  • ✅ DOM変更時に自動でスタイル更新
  • ✅ パフォーマンスも良好

まとめ

CSS :has()は単なる親セレクタではありません。スタイリングロジックの表現方法自体が変わったのです。20年間JavaScriptで無理やり解決していたことが、CSSだけでできるようになりました。

ポイント整理:

  1. :has()関係セレクタです。親、兄弟、子孫、全部条件にできます。
  2. ブラウザサポート95%以上。今すぐプロダクションで使えます。
  3. パフォーマンスは? 具体的なセレクタを使い、深いネストを避ければ問題なし。
  4. JavaScriptコードが減ります。状態クラスのトグル、もう不要です。
  5. CSSネスティング、Container Queriesと組み合わせるとさらに強力に。

今すぐあなたのコードでJavaScriptでクラスをトグルしている部分を探してみてください。:has()で置き換えられるか確認してみましょう。思った以上に多くの場所で使えるはずです。

CSSWeb DevelopmentFrontendPerformanceCSS Selectors

関連ツールを見る

Pockitの無料開発者ツールを試してみましょう