CSS :has() セレクタ実践ガイド ― JavaScript なしで親要素・兄弟要素を自在に操る

CSSだけで「親要素のスタイル変更」「前方の兄弟要素の選択」「子要素の数に応じたレイアウト切替」ができる時代になりました。:has() セレクタの基本から現場で即使えるパターンまで、ライブデモ付きで徹底解説します。

:has() とは何か ― CSSにやっと来た「親セレクタ」

CSSの歴史の中で、「親要素の中身に応じて親自身のスタイルを変えたい」というのは、長年にわたる悲願でした。たとえば「画像を含むカードだけレイアウトを横並びにしたい」「入力が不正なフォームだけ赤枠にしたい」といった場面です。これまではJavaScriptでクラスを付け外しするしかなかったんですが、:has() セレクタの登場で、ついにCSSだけで実現できるようになりました。

:has() は「関係疑似クラス(relational pseudo-class)」と呼ばれ、カッコの中に書いたセレクタに一致する子孫を持つ要素を選択します。シンプルに言えば、「こういう子どもを持っている親を選ぶ」セレクタです。

:has() の基本構文
/* img を子要素に持つ .card を選択 */
.card:has(img) {
  display: flex;
  flex-direction: row;
}

/* チェック済みの input を持つ form を選択 */
form:has(input:checked) {
  background: #f0fdf4;
}

従来のCSSセレクタは「上から下へ」、つまり親から子への方向にしかスタイルを伝搬できませんでした。:has() はこの制約を打ち破り、「下から上へ」子の状態に応じて親をスタイリングできる画期的な仕組みです。

ブラウザサポート状況

2026年3月現在、:has() はすべての主要ブラウザで完全にサポートされています。Chrome 105以降、Edge 105以降、Safari 15.4以降、Firefox 121以降で動作します。Can I Use上のグローバルサポート率は約96%に達しており、実務で安心して採用できる水準です。IE11はもちろん非対応ですが、もうIE対応が必要なプロジェクトはほとんどないでしょう。

💡 現場の経験則
もし古いブラウザへの配慮が必要な案件であっても、@supports selector(:has(*)) でフィーチャーディテクション(機能検出)ができます。:has() が使えないブラウザには従来のスタイルを適用し、使えるブラウザにだけ拡張スタイルを当てるプログレッシブ・エンハンスメント戦略がおすすめです。

パターン1:親セレクタとして使う

:has() の最も基本的で強力な使い方は、「特定の子要素を持つ親を選択する」パターンです。実務で頻出するのは、カードコンポーネントの中身に応じてレイアウトを切り替えるケースでしょう。

画像ありカード / 画像なしカードでレイアウトを変える

たとえばブログのカード一覧で、画像付きのカードは横並び、画像なしのカードは縦積みにしたい場面。従来はJavaScriptか、あるいはサーバー側で別々のクラスを振る必要がありました。:has() を使えば、HTMLは完全に同じ構造のまま、CSSだけでスタイルを分岐できます。

画像の有無でカードのレイアウトを切り替え
/* 画像を含むカード → 横並びレイアウト */
.card:has(img) {
  display: flex;
  flex-direction: row;
  gap: 20px;
}

/* 画像を含まないカード → 通常の縦積み */
.card:not(:has(img)) {
  padding: 24px;
  border-left: 4px solid #059669;
}
▶ Live Demo — 画像の有無でカードレイアウトが変わる
Sample image

画像ありのカード

:has(img) にマッチ → 横並びレイアウトが適用されます。画像とテキストがflexで横に並んでいます。

画像なしのカード

:not(:has(img)) にマッチ → 左ボーダー付きの縦積みレイアウトが適用されています。HTMLの構造は同じで、画像の有無だけで見た目が変わります。

Sample photo

もうひとつの画像ありカード

サムネイルの有無はCMSの入稿内容で変わりますが、CSSが自動的にレイアウトを切り替えてくれます。

フォームバリデーションに使う

フォームの中に不正な入力がひとつでもあったら、フォーム全体の見た目を変える。これも :has() の得意技です。

不正な入力がある場合にフォーム全体を赤枠に
form:has(input:invalid) {
  border: 2px solid #ef4444;
  background: #fef2f2;
}

form:has(input:invalid) .submit-btn {
  opacity: 0.5;
  pointer-events: none;
}
▶ Live Demo — フォームバリデーション連動
※ 不正な入力があるとフォーム全体の背景が赤くなり、送信ボタンが無効になります
📌 ポイント
:has() の中には :invalid:checked:focus-visible:hover など、あらゆる疑似クラスを組み合わせられます。子要素の「状態」を親に伝搬できる ― これが :has() の最大の強みです。

パターン2:前方の兄弟要素を選択する

:has() の真価は、親セレクタだけにとどまりません。実は「前方の兄弟要素」を選択するという、CSSの長年の夢も実現できます。

従来の隣接兄弟セレクタ + や一般兄弟セレクタ ~ は、HTMLの並び順で「後ろ方向」にしか効きませんでした。しかし :has(+ ...) と組み合わせれば、「前方向」に要素を選べるようになります。

前方の兄弟を選ぶ構文
/* すぐ後ろにチェック済みinputがあるlabelを選択 */
label:has(+ input:checked) {
  color: #059669;
  font-weight: 700;
}

/* すぐ後ろに figure がある p を選択 */
p:has(+ figure) {
  margin-bottom: 8px;
}

この「前方兄弟選択」テクニックは、ホバーエフェクトと組み合わせると非常に面白い演出を作れます。ナビゲーションメニューでホバーした項目の「前後」にも効果をつける、といった使い方が典型的です。

▶ Live Demo — ホバーで前後の兄弟にも効果を波及
Home
About
Works
Blog
Contact

※ 各項目にホバーすると、前後の兄弟にも段階的にエフェクトが波及します

上のデモの核心CSS
/* ホバーした要素自体 */
.item:hover {
  transform: scale(1.12);
}

/* 1つ前の兄弟(:has + 隣接セレクタ)*/
.item:has(+ .item:hover) {
  transform: scale(1.05);
}

/* 1つ後ろの兄弟(従来の隣接セレクタ)*/
.item:hover + .item {
  transform: scale(1.05);
}

/* 2つ前の兄弟 */
.item:has(+ .item + .item:hover) {
  transform: scale(1.02);
}

ポイントは、:has(+ .item:hover) が「すぐ次の兄弟がホバーされている要素」を意味するということです。+ を増やせば2つ前、3つ前…と範囲を広げられます。従来の + と組み合わせることで、ホバー対象の「前後」両方向に波及するエフェクトが作れるわけです。

パターン3:グローバル検知 ― ページ全体を制御する

:has()htmlbody に適用すると、「ページのどこかにある要素の状態」に応じてドキュメント全体のスタイルを切り替えられます。これはJavaScriptのイベントリスナーに相当する力です。

モーダル表示時にスクロールをロック

従来であればReactの useEffect やバニラJSで document.body.style.overflow = 'hidden' と書いていたスクロールロック。:has() ならCSSの1行で片付きます。

モーダル表示中のスクロールロック
/* モーダルが表示されていればスクロール無効化 */
html:has(.modal.is-open) {
  overflow: hidden;
}

/* data属性を使うパターン */
html:has([data-scroll-lock="true"]) {
  overflow: hidden;
}

JavaScriptなしのダークモード切替

もっと面白い例を見てみましょう。チェックボックスの状態をグローバルに検知すれば、JavaScriptを一行も書かずにダークモード切替が実現できます。

:has() で実現するJSなしダークモード
/* ライトモード(デフォルト) */
body {
  --bg: #ffffff;
  --text: #1a1a1a;
  --surface: #f4f4f5;
}

/* ダークモード */
body:has(#dark-toggle:checked) {
  --bg: #0e0e0e;
  --text: #e2e8f0;
  --surface: #1e293b;
}
▶ Live Demo — JSなしダークモード切替

Sample Page

記事タイトル A

CSSカスタムプロパティと :has() を組み合わせることで、JSなしのテーマ切替が可能になります。

記事タイトル B

チェックボックスの :checked 状態をグローバルに検知しているため、ページ内のあらゆる要素に反映できます。

⚠️ 注意
このJSなしダークモードはデモとしては秀逸ですが、本番運用ではユーザーの選択を localStorage に保存したり、OSの prefers-color-scheme と連動させたりするJavaScriptが別途必要です。コアなテーマ切替ロジックをCSSに寄せつつ、永続化の部分だけJSに任せるハイブリッド構成がベストプラクティスです。

パターン4:数量クエリ ― 子要素の数でスタイルを変える

:has():nth-child() と組み合わせると、「子要素の数に応じて親のスタイルを変える」という、いわゆる数量クエリ(Quantity Queries)が実現できます。これは地味に見えて、実務でものすごく役に立つパターンです。

たとえばタグ一覧。3つ以下なら横に並べたいけど、4つ以上になったら折り返して表示したい。あるいはギャラリーの枚数によってグリッドのカラム数を変えたい。こうした「コンテンツ量に応じたレイアウト」をCSSだけで書けるようになります。

子要素の数に応じたスタイル分岐
/* 子要素が4つ以上ある場合 */
.tag-list:has(> :nth-child(4)) {
  flex-wrap: wrap;
}

/* 子要素がちょうど3つの場合 */
.grid:has(> :nth-child(3):last-child) {
  grid-template-columns: repeat(3, 1fr);
}

/* 子要素が6つ以上ある場合 */
.grid:has(> :nth-child(6)) {
  grid-template-columns: repeat(3, 1fr);
}
▶ Live Demo — 要素数でグリッドレイアウトが変化

2個 → 2列 / 4個以上 → 4列 / 7個以上 → 3列。ボタンを押してアイテム数を切り替えてみてください。

1
2
3
4
5

このパターンが優れているのは、「HTMLを生成する側はアイテムの数を気にせずそのまま出力するだけ」という点です。CMS出力やAPIレスポンスに応じて、CSSが自動的に最適なグリッドを選んでくれる。まさにコンテンツドリブンなレイアウトです。

パターン5:「自分以外すべて」セレクタと実務テクニック

最後にもう一つ、現場で活躍するパターンを紹介します。:has():not() を組み合わせた「自分以外すべて」セレクタです。

たとえばカード一覧で、ホバーしたカード以外をぼかして、注目したい要素を際立たせるUIを作りたい場面。CSSだけで、しかもたった1行のセレクタで実現できます。

「自分以外すべて」をぼかすセレクタ
/* コンテナ内でホバーされたカードがある時、
   ホバーされていないカードすべてをぼかす */
.card-list:has(.card:hover) .card:not(:hover) {
  filter: blur(3px);
  opacity: 0.6;
}
▶ Live Demo — ホバーしたカード以外をぼかすUI
プロジェクト A

コーポレートサイトのリニューアル案件。Grid + :has() でカードUIを構築。

プロジェクト B

ECサイトの商品一覧。画像の有無でカードの見た目を自動切替。

プロジェクト C

SaaSダッシュボード。フォームバリデーションを :has() で実装。

※ カードにホバーすると、他のカードがぼけてフォーカスされます

:has() を使う際の注意点

:has() は強力ですが、使いすぎには注意が必要です。特に意識すべきポイントをいくつか挙げます。

❌ やりがちなNG::has() のネスト
/* :has() の中に :has() はネストできない! */
.parent:has(.child:has(span)) {
  color: red;
  /* ❌ Invalid — ブラウザに無視されます */
}
✅ 正しい書き方::has() のチェーン
/* :has() をチェーン(連結)するのはOK */
.parent:has(.child):has(span) {
  color: red;
  /* ✅ Valid — .child と span の両方を子孫に持つ .parent */
}
💡 現場の経験則
:has() は「CSSでできることが広がる」セレクタですが、すべてをCSSに寄せればいいというわけではありません。複雑なインタラクションロジックや、動的なカテゴリフィルタリングなどはJavaScriptのほうがシンプルに書けることも多いです。判断基準は「このCSSセレクタを半年後の自分(または同僚)が読んで、すぐ理解できるか?」です。読みやすさと保守性を最優先に選択してください。

まとめ

  • :has() は「特定の子要素を持つ親」を選択する関係疑似クラス。CSSにやっと来た「親セレクタ」です。
  • 2026年3月現在、Chrome / Edge / Safari / Firefox すべてで安定サポート。実務で安心して使えます。
  • フォームバリデーション、カードレイアウトの自動切替、ダークモードなど、JavaScriptなしで実現できる場面が大幅に増えます。
  • :has(+ .sibling) で「前方の兄弟」を選べる。従来のCSSでは不可能だった双方向のスタイリングが可能に。
  • html:has(...) でグローバル検知。モーダル表示中のスクロールロックもCSSだけで実装できます。
  • :nth-child() と組み合わせた数量クエリで、子要素の数に応じたレスポンシブなレイアウト分岐も可能。
  • ネスト不可・パフォーマンス・可読性に注意しつつ、「CSSでシンプルに書けるか?」を判断基準にJSとの使い分けを意識しましょう。