:has()は、「中に○○がある要素」を選べる唯一のCSSセレクターです。2023年12月の Firefox 121 対応で全モダンブラウザが揃い、今は Baseline として安心して使えます。
正確に言うと「親セレクター」ではありません。直前の兄弟や、条件を満たす祖先も選べる関係疑似クラスです。この記事では、基本構文と誤解しやすい点、実際に効く7つのデザインパターン、そして:has()特有の落とし穴(詳細度、使えない場所)を整理します。
スポンサーリンク
:has() は何を選ぶのか
A:has(B)は、「Bにマッチする要素が見つかるようなA」を選びます。スタイルが当たるのはAであって、Bではありません。
.container:has(.child) {
background-color: lightblue;
}
ここでよくある誤解が「子要素を持つ場合」という説明です。実際は子孫全体が対象で、何階層下にあってもマッチします。直下の子だけに限定したい場合は、>を明示します。
/* 何階層下でもマッチする */
.container:has(.child) { }
/* 直下の子だけ */
.container:has(> .child) { }
/* 直後の兄弟がある要素 */
h2:has(+ p) { }
/* 後続のどこかに兄弟がある要素 */
h2:has(~ .note) { }
この+と~が使えることが重要です。CSSで「前の要素」を選ぶ手段は:has()しかありません。
対応状況
| ブラウザ | バージョン | リリース |
|---|---|---|
| Safari | 15.4 | 2022年3月 |
| Chrome / Edge | 105 | 2022年9月 |
| Firefox | 121 | 2023年12月 |
パターン1:入力状態に応じて親の見た目を変える
最も使用頻度が高いのがこれです。チェックされたカードを強調する、といった実装がCSSだけで書けます。
<label class="card">
<input type="checkbox" name="plan" value="a">
<span>スタンダードプラン</span>
</label>
.card:has(input:checked) {
border-color: #2a7;
background: color-mix(in oklab, #2a7 8%, canvas);
}
/* 無効なカードは全体を薄く */
.card:has(input:disabled) {
opacity: .5;
cursor: not-allowed;
}
従来はJavaScriptでchangeを監視してクラスを付け外ししていた処理が、丸ごと不要になります。
スポンサーリンク
パターン2:エラーのある行だけを強調する
フォームの検証と組み合わせると効果的です。:invalidではなく:user-invalidを使うのがポイントで、こうするとページを開いた瞬間に全行が赤くなる、という事故を防げます。
/* ユーザーが操作したあとに不正な入力を含む行だけ赤くする */
.field:has(:user-invalid) {
border-left: 3px solid #d33;
background: #fff5f5;
}
.field:has(:user-invalid) .error-message {
display: block;
}
/* 必須項目の行にマークを出す */
.field:has([required]) label::after {
content: " *";
color: #d33;
}
送信ボタンの制御にも使えます。
/* フォーム内に不正な値があればボタンを押せない見た目にする */
form:has(:invalid) button[type="submit"] {
opacity: .5;
pointer-events: none;
}
ここは:user-invalidではなく:invalidが適切です。未入力の必須項目がある間はボタンを無効にしたいためです。「入力欄の見た目は:user-invalid、送信可否の判定は:invalid」と覚えておくと迷いません。
パターン3:直前の見出しの余白を詰める
:has()が無い時代、「見出しの直後に画像が来るときだけ余白を変える」といった調整はクラスを手で足すしかありませんでした。
/* 直後に図がある見出しは下余白を詰める */
h2:has(+ figure) {
margin-bottom: .5rem;
}
/* 見出しが連続している場合(h2直後にh3)の詰め */
h2:has(+ h3) {
margin-bottom: .25rem;
}
/* 画像しか入っていない段落は余白を消す */
p:has(> img:only-child) {
margin: 0;
}
CMSやMarkdownから生成される本文のように、HTMLの構造をこちらで制御できない場面で特に威力を発揮します。
スポンサーリンク
パターン4:モーダル表示中に背景のスクロールを止める
<dialog>のshowModal()は背景の操作を無効化しますが、スクロールは止めません。:has()を使うとJavaScript無しで対応できます。
body:has(dialog[open]) {
overflow: hidden;
}
/* ポップオーバー表示中に背景を暗くする */
body:has([popover]:popover-open)::after {
content: "";
position: fixed;
inset: 0;
background: rgb(0 0 0 / .3);
}
この「状態を持つ要素を探して、ルート側のスタイルを変える」使い方は、:has()でしか書けません。ダイアログの実装はdialog要素の改善解説と使い方にまとめています。
パターン5:要素の数でレイアウトを変える(数量クエリ)
:has()と:nth-child()を組み合わせると、子要素の数によってレイアウトを切り替えられます。
/* 子が1つだけなら1カラム */
.grid:has(> :only-child) {
grid-template-columns: 1fr;
}
/* 子が4つ以上あるとき(4番目が存在する)は3カラム */
.grid:has(> :nth-child(4)) {
grid-template-columns: repeat(3, 1fr);
}
「アイテムが1つのときだけ中央に大きく置く」といった、これまでJavaScriptで件数を数えて分岐していた処理を置き換えられます。
パターン6:サイドバーの有無でレイアウトを切り替える
.layout {
display: grid;
grid-template-columns: 1fr;
}
.layout:has(> .sidebar) {
grid-template-columns: 1fr 280px;
gap: 32px;
}
@media (max-width: 768px) {
.layout:has(> .sidebar) {
grid-template-columns: 1fr;
}
}
テンプレート側で.has-sidebarのようなクラスを付け分ける必要が無くなります。HTMLの構造がそのまま条件になるので、クラス名の付け忘れによる崩れが起きません。
パターン7::not() と組み合わせて「〜が無い場合」を選ぶ
:has()の否定は2通りの書き方があり、意味が違います。ここは間違えやすいので注意してください。
/* ① 画像を「持たない」カード */
.card:not(:has(img)) {
padding: 24px;
}
/* ② 「画像以外の何か」を持つカード(画像も持っていてよい) */
.card:has(:not(img)) {
/* ほぼ全てのカードにマッチしてしまう */
}
「〜が無いとき」を表したいなら、必ず①の:not(:has(...))の順です。②は意図しない広範囲にマッチします。
詳細度の計算に注意する
:has()自体は詳細度を持ちませんが、引数の中で最も詳細度の高いセレクターがそのまま加算されます。:is()や:not()と同じ挙動です。
| セレクター | 詳細度 |
|---|---|
.card:has(img) |
0,1,1(クラス1+要素1) |
.card:has(.badge) |
0,2,0(クラス2) |
.card:has(#promo) |
1,1,0(ID1+クラス1) |
引数にIDを入れると詳細度が跳ね上がり、あとから上書きできなくなります。:has()の中にIDセレクターを書かないのが安全です。
:has() が使えない場所
仕様上、次の使い方はできません。エラーになるか、無視されます。
:has()の中に:has()を入れる(入れ子は不可)- 擬似要素に付ける:
::before:has(...)は不可 :has()の引数に擬似要素を書く::has(::before)は不可- 引数に
:visitedを書く:プライバシー保護のため無視されます
また、<html>や<body>に:has()を書くとページ全体の再計算対象になります。動作はしますが、頻繁に変化する条件(マウス移動に連動するなど)と組み合わせるのは避けた方が無難です。
パフォーマンスをどう考えるか
結論として、通常の使い方で問題になることはほぼありません。ブラウザ側でセレクターの評価は十分に最適化されています。
そのうえで、避けた方がよい書き方は次の2つです。
/* ❌ 全要素が評価対象になる */
*:has(div) { color: red; }
/* ❌ 条件が広すぎる */
div:has(span) { }
/* ✅ 起点を絞る */
.card:has(> .badge) { }
ポイントは「起点(:has()の左側)を具体的なクラスに絞る」ことと、「可能なら>や+で探索範囲を限定する」ことです。この2つを守れば、計測して問題になるようなことはまず起きません。
非対応ブラウザへの配慮
2026年時点で対応していないのは、事実上サポート対象外の古いブラウザだけです。それでも装飾が壊れると困る場合は、@supports selector()で分岐します。
@supports selector(:has(a)) {
.layout:has(> .sidebar) {
grid-template-columns: 1fr 280px;
}
}
@supports (...)ではなく@supports selector(...)である点に注意してください。セレクターの対応可否を調べる構文は別物です。
まとめ
A:has(B)はBを含むAを選ぶ。子だけに限定するなら:has(> B)と書く+や~を使えば「前の要素」を選べる。これはCSSで:has()だけができること- 入力状態(
:checked/:user-invalid)と組み合わせるとJavaScriptが不要になる - 「〜が無い」は必ず
:not(:has(...))の順で書く - 引数にIDを入れると詳細度が跳ね上がる。入れない
:has()の入れ子、擬似要素との併用、:visitedは不可- 機能検出は
@supports selector(:has(a))
「CSSでは無理だからJavaScript」と判断していた実装の多くが、:has()で置き換えられます。フォームまわりの実装はHTML form要素の新機能と活用法、色の指定はCSS color()関数の使い方もあわせてどうぞ。