- CSS
- フロントエンド
- 技術 💻
CSS :has() 使い方|実務パターン15選と検証結果
CSSの :has() とは、「指定した条件に当てはまる要素を中に(または後ろに)持つ要素」を選ぶ擬似クラスです。これまでJavaScriptが必要だった「子の状態に応じて親の見た目を変える」「後ろの兄弟に応じて前の要素を変える」がCSSだけで書けます。
この記事では、実務でよく使う 15パターンをコピペ用のHTML+CSSで紹介 し、すべて Playwright + Chromium 141 で 適用前と適用後の getComputedStyle の値を実測 しました(画像の代わりに値で結果を示しています)。あわせて、:is() / :where() との違い、詳細度の意外な決まり方、無効なセレクタを含めたときの挙動、対応状況(Baseline)、@supports でのフォールバック、パフォーマンスの注意点まで実行結果つきでまとめます。
:has() 実務パターン早見表
| # | やりたいこと | セレクタ | 検証(適用前 → 適用後) |
|---|---|---|---|
| 1 | 入力エラーの欄の枠を赤く | .field:has(input:invalid:not(:placeholder-shown)) |
枠色 rgb(203, 213, 225) → rgb(220, 38, 38) |
| 2 | 必須が未入力の間は送信ボタンをグレー | form:has(:required:placeholder-shown) button[type="submit"] |
未入力 rgb(148, 163, 184) → 入力後 rgb(37, 99, 235) |
| 3 | 画像があるカードだけ2列に | .card:has(> img) |
列 1264px → 120px 1144px |
| 4 | チェックでメニューを開く(JSなし) | .nav:has(#menu-toggle:checked) .menu |
display: none → block |
| 5 | モーダル表示中はページをスクロールさせない | html:has(dialog[open]) |
overflow: visible → hidden |
| 6 | 子が4個以上なら2列に | .grid:has(> :nth-child(4)) |
3列 → 2列(632px 632px) |
| 7 | 後ろの兄弟に応じて前の要素を変える | h2:has(+ .lead) |
下余白 24px → 4px |
| 8 | 中身が空のときメッセージを出す | .list:not(:has(> li)) + .empty-msg |
display: none → block |
| 9 | チェックボックスでダークモード | :root:has(#dark:checked) |
背景 rgb(255, 255, 255) → rgb(17, 24, 39) |
| 10 | チェックした行をハイライト | tr:has(input[type="checkbox"]:checked) |
背景 透明 → rgb(254, 249, 195) |
| 11 | 選択中のプランカードを強調 | .plan:has(input[type="radio"]:checked) |
枠色 グレー → rgb(37, 99, 235) |
| 12 | ホバー中以外の画像を薄く | .gallery:has(img:hover) img:not(:hover) |
opacity: 1 → 0.4 |
| 13 | 入力中の検索欄を強調 | .search:has(input:focus-visible) |
影なし → 0 0 0 3px の枠 |
| 14 | 必須項目のラベルに「*」 | label:has(+ input:required)::after |
content: none → " *" |
| 15 | すべてチェックしたら完了表示 | .tasks:not(:has(input:not(:checked))) .done-msg |
display: none → block |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「CSS :has() 使い方|実務パターン15選と検証結果」
:has() の基本と書き方
| 書き方 | 意味 | 例 |
|---|---|---|
A:has(B) |
子孫に B を持つ A | .card:has(img) |
A:has(> B) |
直下の子に B を持つ A | .card:has(> img) |
A:has(+ B) |
すぐ後ろの兄弟が B の A | h2:has(+ .lead) |
A:has(~ B) |
後ろのどこかの兄弟に B がある A | .step:has(~ .step.done) |
A:has(B, C) |
B または C を持つ A | article:has(video, audio) |
A:has(B):has(C) |
B と C の両方を持つ A | article:has(video):has(audio) |
A:not(:has(B)) |
B を持たない A | .list:not(:has(li)) |
「親セレクタ」と呼ばれることが多いですが、+ や ~ を使うと「前の兄弟」も選べます。
検証環境
| 項目 | 内容 |
|---|---|
| ブラウザ | Chromium 141.0.7390.37(Playwright 1.56) |
| 確認方法 | 操作の前後で getComputedStyle(要素).getPropertyValue(プロパティ) を取得 |
| ページ | 下の15パターンを1つのHTMLにまとめ、file:// で開いて実行 |
// 確認に使った関数(Playwright)
const cs = (sel, prop, pseudo = null) => page.evaluate(([s, pr, ps]) =>
getComputedStyle(document.querySelector(s), ps).getPropertyValue(pr), [sel, prop, pseudo]);
フォームで使う :has()
パターン1:入力エラーのある欄の枠を赤くする
<div class="field"><input type="email" placeholder="mail@example.com"></div>
.field { border: 2px solid rgb(203, 213, 225); }
.field:has(input:invalid:not(:placeholder-shown)) { border-color: rgb(220, 38, 38); }
| 状態 | border-top-color |
|---|---|
| 未入力 | rgb(203, 213, 225) |
abc と入力(メール形式でない) |
rgb(220, 38, 38) |
:not(:placeholder-shown) を付けると、何も入力していない初期状態では赤くなりません。入力欄そのものではなく、囲んでいる .field(ラベルやヘルプ文を含む枠)の見た目を変えられるのが :has() の利点です。操作後にだけ判定したい場合は :user-invalid も選択肢です。
パターン2:必須が未入力の間は送信ボタンをグレーにする
<form class="p2">
<input required placeholder="氏名">
<button type="submit">送信</button>
</form>
form.p2 button[type="submit"] { background-color: rgb(37, 99, 235); }
form.p2:has(:required:placeholder-shown) button[type="submit"] {
background-color: rgb(148, 163, 184);
pointer-events: none;
}
| 状態 | ボタンの background-color |
|---|---|
| 氏名が空 | rgb(148, 163, 184)(グレー) |
| 「山田」と入力 | rgb(37, 99, 235)(青) |
見た目の制御なので、送信の可否は必ずサーバー側やJSのバリデーションでも確認 します。pointer-events: none だけではキーボード(Enterキー)での送信は止まりません。
パターン14:必須項目のラベルに「*」を付ける
<label for="n2">氏名</label><input id="n2" required>
<label for="n3">備考</label><input id="n3">
label:has(+ input:required)::after { content: " *"; color: rgb(220, 38, 38); }
| ラベル | ::after の content |
|---|---|
| 備考(任意) | none |
| 氏名(必須) | " *" |
HTMLの required 属性だけを管理すれば、ラベルの印が自動でそろいます。:has() の後ろに ::after を付けるのは有効です(:has() の中に擬似要素を書くのは無効。後述)。
パターン13:入力中の検索欄を強調する
<div class="search"><input placeholder="検索"></div>
.search:has(input:focus-visible) { box-shadow: rgb(37, 99, 235) 0px 0px 0px 3px; }
| 状態 | box-shadow |
|---|---|
| フォーカスなし | none |
| 入力欄にフォーカス | rgb(37, 99, 235) 0px 0px 0px 3px |
単に「中のどれかにフォーカスがある」なら :focus-within でも書けます。:has() を使うと「入力欄の :focus-visible のときだけ」「特定の子のときだけ」と条件を絞れます。
レイアウトを切り替える :has()
パターン3:画像があるカードだけ2列にする
<div class="card"><img src="thumb.jpg" alt=""><p>画像あり</p></div>
<div class="card"><p>画像なし</p></div>
.card { display: grid; grid-template-columns: 1fr; }
.card:has(> img) { grid-template-columns: 120px 1fr; }
| カード | grid-template-columns(幅1280pxの画面で計測) |
|---|---|
| 画像なし | 1264px |
| 画像あり | 120px 1144px |
CMSから出力されるHTMLが同じクラス名でも、中身に応じてレイアウトが変わります。サーバー側で card--with-image のようなクラスを付け分ける処理が不要になります。
パターン6:子の数でグリッドの列数を変える
<div class="grid"><div>1</div><div>2</div><div>3</div></div>
<div class="grid"><div>1</div><div>2</div><div>3</div><div>4</div></div>
.grid { display: grid; grid-template-columns: repeat(3, 1fr); }
.grid:has(> :nth-child(4)) { grid-template-columns: repeat(2, 1fr); }
| 子の数 | grid-template-columns |
|---|---|
| 3個 | 421.328px 421.328px 421.344px(3列) |
| 4個 | 632px 632px(2列) |
:has(> :nth-child(4)) は「4番目の子がある」=「子が4個以上」という意味 です。「ちょうど4個」にしたい場合は :has(> :nth-child(4):last-child) と組み合わせます。
パターン7:後ろの兄弟に応じて前の要素を変える
<h2>見出しA</h2><p class="lead">リード文</p>
<h2>見出しB</h2><p>本文</p>
h2 { margin-bottom: 24px; }
h2:has(+ .lead) { margin-bottom: 4px; }
| 見出し | margin-bottom |
|---|---|
| 後ろが通常の段落 | 24px |
後ろが .lead |
4px |
+ で「すぐ後ろの兄弟」、~ で「後ろのどこかの兄弟」を条件にできます。見出しとリード文を近づける、といった余白調整に便利です。
パターン8:中身が空のときメッセージを出す
<ul class="list"></ul>
<p class="empty-msg">まだタスクはありません</p>
.empty-msg { display: none; }
.list:not(:has(> li)) + .empty-msg { display: block; }
| リスト | .empty-msg の display |
|---|---|
li が1件ある |
none |
li が0件 |
block |
:empty は空白文字やコメントがあると一致しませんが、:not(:has(> li)) なら「li がない」を直接判定できます。
JavaScriptなしで状態を切り替える :has()
パターン4:チェックでメニューを開閉する
<nav class="nav">
<label><input type="checkbox" id="menu-toggle">メニュー</label>
<ul class="menu"><li>ホーム</li></ul>
</nav>
.nav .menu { display: none; }
.nav:has(#menu-toggle:checked) .menu { display: block; }
| 状態 | .menu の display |
|---|---|
| 未チェック | none |
| チェック | block |
チェックボックスとメニューが兄弟でなくても、共通の親を起点に書けます。以前の input:checked ~ .menu はHTMLの並び順に制約がありましたが、:has() なら位置を自由にできます。本番では、開閉状態を支援技術に伝えるため <details> やボタン+aria-expanded も検討します。
パターン5:モーダル表示中はページをスクロールさせない
<dialog id="dlg">モーダル</dialog>
<script>document.getElementById('dlg').showModal();</script>
html:has(dialog[open]) { overflow: hidden; }
| 状態 | html の overflow |
|---|---|
| 閉じている | visible |
showModal() 後 |
hidden |
開閉のJSにスクロール固定の処理を書き足す必要がなくなります。ページ全体(html)を起点にするセレクタなので、中の条件は dialog[open] のように 絞り込んだもの にしておきます(パフォーマンスの節を参照)。
パターン9:チェックボックスでダークモードを切り替える
<label><input type="checkbox" id="dark">ダークモード</label>
<div class="page">ページ</div>
:root { --bg: rgb(255, 255, 255); }
:root:has(#dark:checked) { --bg: rgb(17, 24, 39); }
.page { background-color: var(--bg); }
| 状態 | .page の background-color |
|---|---|
| 未チェック | rgb(255, 255, 255) |
| チェック | rgb(17, 24, 39) |
CSS変数(カスタムプロパティ)を :root で切り替えると、ページ中の色がまとめて変わります。選択を次回も保持したい場合は、JSで localStorage に保存して復元します。
パターン10:チェックした行をハイライトする
<table><tbody>
<tr><td><input type="checkbox"></td><td>備品A</td></tr>
</tbody></table>
tr:has(input[type="checkbox"]:checked) { background-color: rgb(254, 249, 195); }
| 状態 | tr の background-color |
|---|---|
| 未チェック | rgba(0, 0, 0, 0) |
| チェック | rgb(254, 249, 195) |
パターン11:選択中のプランカードを強調する
<label class="plan"><input type="radio" name="plan">ライト</label>
<label class="plan"><input type="radio" name="plan">スタンダード</label>
.plan { border: 2px solid rgb(203, 213, 225); }
.plan:has(input[type="radio"]:checked) { border-color: rgb(37, 99, 235); }
| 状態 | 2枚目の border-top-color |
|---|---|
| 未選択 | rgb(203, 213, 225) |
| 2枚目を選択 | rgb(37, 99, 235) |
パターン12:ホバー中以外の画像を薄くする
<div class="gallery"><img src="a.jpg" alt=""><img src="b.jpg" alt=""></div>
.gallery:has(img:hover) img:not(:hover) { opacity: 0.4; }
| 状態 | 2枚目の opacity |
|---|---|
| どこにもホバーしていない | 1 |
| 1枚目にホバー | 0.4 |
「ギャラリーのどれかにホバー中」を親で判定し、ホバーしていないものだけ薄くします。
パターン15:すべてチェックしたら完了メッセージを出す
<div class="tasks">
<label><input type="checkbox">A</label>
<label><input type="checkbox">B</label>
<p class="done-msg">すべて完了しました</p>
</div>
.done-msg { display: none; }
.tasks:not(:has(input[type="checkbox"]:not(:checked))) .done-msg { display: block; }
| 状態 | .done-msg の display |
|---|---|
| 未チェックが残っている | none |
| A・Bともチェック | block |
「未チェックのものを持たない」=「すべてチェック済み」という二重否定の書き方 です。
:is() / :where() との違いと詳細度
| 擬似クラス | 選ぶ対象 | 詳細度 | 無効なセレクタを含むと |
|---|---|---|---|
:is(A, B) |
A または B に自分が一致 | 引数の中で最も高いもの | 無効な部分だけ無視(寛容) |
:where(A, B) |
同上 | 常に0 | 無効な部分だけ無視(寛容) |
:has(A, B) |
A または B を中や後ろに持つ | 引数の中で最も高いもの | 規則全体が無効(Chromium 141で確認) |
:not(A, B) |
A でも B でもない | 引数の中で最も高いもの | ― |
検証1:無効なセレクタを混ぜたとき
.x:has(.a, :foo) { color: rgb(255, 0, 0); } /* :foo は存在しない擬似クラス */
.x:is(.a, :foo) { color: rgb(0, 128, 0); }
スタイルシートの cssRules を読み出した結果です。
[ '.x:is(.a)', '.box:has(#target)', … ]
:is() の規則は :foo だけを取り除いて .x:is(.a) として残りましたが、:has() の規則は丸ごと読み込まれませんでした。:has() の中で新しい擬似クラスを使うときは、未対応ブラウザで規則全体が無視されることを想定しておきます。
検証2:詳細度は「一致した引数」ではなく「最も高い引数」
<div class="box3 k l"><span class="item">I</span></div>
.box3:has(> .item, .item .deep #t3) { outline: 1px solid rgb(0, 0, 0); }
.box3.k.l { outline-color: rgb(0, 128, 0); }
b3 outlineColor => rgb(0, 0, 0)
実際に一致したのは > .item ですが、詳細度は引数の中で最も高い .item .deep #t3(IDを含む)で計算されるため、クラス3つの .box3.k.l に勝ちました。意図しない上書きを防ぐには、ID を引数に入れない、または :where() で包みます。
検証3::where() で包むと詳細度を0にできる
.box2:where(:has(#t2)) { color: rgb(220, 38, 38); }
div.box2 { color: rgb(37, 99, 235); }
.box:has(#target) { color: rgb(220, 38, 38); }
.box.alt.more { color: rgb(37, 99, 235); }
| 比較 | 結果の color |
勝った側 |
|---|---|---|
.box:has(#target) 対 .box.alt.more |
rgb(220, 38, 38) |
:has() 側(ID 1つ>クラス3つ) |
.box2:where(:has(#t2)) 対 div.box2 |
rgb(37, 99, 235) |
div.box2 側(:where 部分は0) |
ライブラリやデザインシステムで「上書きしやすい既定スタイル」を作るときは :where(:has(…)) が便利 です。
書けないこと・注意点
| 書き方 | 結果(Chromium 141の CSS.supports) |
理由 |
|---|---|---|
:has(a) |
true |
通常の使い方 |
:has(:has(a)) |
false |
:has() の入れ子は不可 |
:has(::before) |
false |
引数に擬似要素は不可 |
:has(.a, :foo) |
false |
無効な引数を含む |
label:has(+ input)::after |
有効(パターン14で動作) | 擬似要素を :has() の後ろに付けるのは可 |
対応状況(Baseline)とフォールバック
npm の web-features と @mdn/browser-compat-data(8.1.3)で確認した結果です。
| ブラウザ | 対応バージョン |
|---|---|
| Chrome / Edge / Chrome for Android | 105 |
| Safari / Safari on iOS | 15.4 |
| Firefox / Firefox for Android | 121 |
| Baseline | 日付 |
|---|---|
| Newly available(全主要ブラウザ対応) | 2023-12-19 |
| Widely available | 2026-06-19 |
2026年6月に Baseline Widely available になり、主要ブラウザの現行版では安心して使える段階 です。古い端末も対象にする場合は @supports selector() で切り替えます。
/* :has() が使えないブラウザ向けの表示 */
@supports not selector(:has(a)) {
.fallback { display: block; }
}
/* :has() が使えるブラウザでは隠す */
@supports selector(:has(a)) {
.fallback { display: none; }
}
Chromium 141 では .fallback の display が none(:has() 対応側)になりました。
// JavaScript で判定する場合
if (CSS.supports('selector(:has(a))')) {
// :has() 対応
}
パフォーマンスの注意点
:has() は子や兄弟の変化で親のスタイルを再計算するため、「どこを起点に」「どこまで探すか」を絞る のが基本です。MDNの :has() ページでも、広い起点(body、:root、*)を避け、> や + で探索範囲を限定する書き方が推奨されています。
| 書き方 | 探す範囲 | おすすめ度 |
|---|---|---|
*:has(.item) |
すべての要素の全子孫 | 避ける |
body:has(.item) |
ページ全体 | 必要なときだけ、条件を絞る |
.card:has(.item) |
カードの全子孫 | 標準 |
.card:has(> .item) |
カードの直下の子だけ | より軽い |
.card:has(+ .item) |
すぐ後ろの兄弟だけ | より軽い |
ページ全体の状態(モーダル、テーマ)を扱うときは html:has(dialog[open]) や :root:has(#dark:checked) のように、引数を属性やIDで絞る と探索の対象が限定されます。大量の行を持つ一覧で :has() を使う場合は、DevTools の Performance パネルで「Recalculate Style」の時間を確認してから採用すると安心です(本記事では速度の計測は行っていません)。
よくある失敗と直し方
| 症状 | 原因 | 直し方 |
|---|---|---|
| 規則がまるごと効かない | 引数に無効・未対応のセレクタが含まれる | 引数を分けて別の規則にする |
| 思ったより強く上書きされる | 引数の中のIDで詳細度が上がる | :where(:has(…)) で包む |
| 初期表示から赤枠が出る | 空欄も :invalid に一致 |
:not(:placeholder-shown) や :user-invalid を併用 |
| 「ちょうどN個」にならない | :nth-child(4) は「4個以上」 |
:nth-child(4):last-child を使う |
| ボタンをグレーにしたのに送信できる | CSSは見た目のみ | disabled 属性やバリデーションを併用 |
:has(:has()) が効かない |
入れ子は仕様上不可 | 条件を1段にまとめる、クラスで補助 |
よくある質問
Q. :has() は「親セレクタ」ですか?
A. 親を選ぶ用途が多いですが、正確には「条件に合う要素を中や後ろに持つ要素」を選びます。h2:has(+ .lead) のように前の兄弟を選ぶこともできます。
Q. :has() はもう本番で使えますか?
A. Chrome/Edge 105、Safari 15.4、Firefox 121 以降で対応し、2026年6月19日に Baseline Widely available になりました。古い端末も対象にする場合は @supports not selector(:has(a)) でフォールバックを用意します。
Q. :has() と :focus-within はどう使い分けますか?
A. 「中のどこかにフォーカスがある」だけなら :focus-within が簡潔です。「入力欄の :focus-visible のときだけ」など条件を絞りたいときに :has() を使います。
Q. :has() を使うとページが重くなりますか?
A. 起点と探索範囲を絞れば通常の用途で問題になることは少ないです。*:has() や body:has(.広い条件) を避け、> や + で範囲を限定します。大規模な一覧ではDevToolsで確認してから採用します。
まとめ
:has()は「条件に合う要素を中や後ろに持つ要素」を選ぶ擬似クラス で、親や前の兄弟をCSSだけで変えられます。- フォーム・カード・メニュー・モーダル・テーブルなど15パターンすべて、Chromium 141 で適用前後の値の変化を確認 しました。
- 詳細度は「一致した引数」ではなく「引数の中で最も高いもの」 になり、
:where()で包むと0にできます。 - 無効なセレクタを含むと
:has()の規則は丸ごと無視され、:is()は無効部分だけ除かれました。 :has()の入れ子と、引数内の擬似要素は使えません(後ろに::afterを付けるのは可)。- 2026年6月に Baseline Widely available。古い環境には
@supports selector(:has(a))で対応します。 - 起点を絞り、
>や+で探索範囲を限定 すると、パフォーマンス面でも扱いやすくなります。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「CSS :has() 使い方|実務パターン15選と検証結果」
株式会社RJCでは、:has() のような新しいCSSの機能を実際に動かして確かめ、プロジェクトの中で仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。