うぇぶわんこ
:has()って、子要素を見て親を選べるCSSだよね? もうJavaScriptでclassを付けなくてもいいワン?
さとけん
使える場面は増えました。でも:has()は親を選ぶだけでも、JavaScriptを全部置き換えるものでもありません。「何を条件に見た目を変えるか」で考えると分かりやすいです。
JavaScriptでclassを付けて、そのclassをCSSで見る。
Web制作では、長いあいだよく使われてきた実装です。
HTMLに状態がある
↓
JavaScriptで判定する
↓
classを付ける
↓
CSSがそのclassを見る
現在は、「中に何があるか」「中の要素がどんな状態か」をCSS側から条件にできる:has()があります。
たとえば、
- 画像があるカードだけレイアウトを変える
- チェックされた項目の親を強調する
- 入力エラーがあるフォームグループを目立たせる
- モーダルが開いたらページ側のスタイルを変える
といった処理です。
ただし、:has()は「JavaScriptが不要になる魔法のセレクタ」ではありません。
すでにHTML上に存在している状態を見て、見た目を切り替えるだけならCSSで扱いやすくなった、というのがポイントです。
また、:has()は「親要素を選べるセレクタ」として紹介されることが多いですが、実際には子要素だけでなく、状態・属性・兄弟関係なども条件にできます。
この記事では、実務で使いやすい7つの例を通して、:has()の使い方とJavaScriptとの役割分担を整理します。
この記事でわかること
:has()は「中や周囲に条件を満たす要素があるか」を見る
まずは基本形です。
.card:has(.card__image) {
padding-top: 0;
}
これは、
.card__imageを含んでいる.card
を選びます。
通常のCSSでは、
.card .card__image {
}
のように、親から子孫へ向かって選ぶことは簡単でした。
一方、「この子要素を持っている親だけを変えたい」という条件は、以前のCSSだけでは書きにくいものでした。
そのため、HTML側へ状態を表すclassを追加する方法がよく使われていました。
<article class="card card--has-image">
<img class="card__image" src="photo.jpg" alt="">
<h2 class="card__title">タイトル</h2>
</article>
.card--has-image {
padding-top: 0;
}
ただ、「画像が入っているかどうか」はすでにHTMLを見れば分かる状態です。
現在なら、
.card:has(.card__image) {
padding-top: 0;
}
とCSS側で直接条件にできます。
HTMLにすでに存在する情報を、見た目のためだけにもう一度classとして持たなくてもよい。
ここが:has()の分かりやすいメリットです。
01|画像があるカードだけレイアウトを変える
まずは、:has()の基本が分かりやすい例です。
画像あり・画像なしのカードが混在しているとします。
<article class="card">
<img class="card__image" src="photo.jpg" alt="">
<h2 class="card__title">お知らせ</h2>
<p class="card__text">本文が入ります。</p>
</article>
<article class="card">
<h2 class="card__title">お知らせ</h2>
<p class="card__text">本文が入ります。</p>
</article>
画像があるカードだけ余白を変えるなら、
.card {
padding: 2rem;
}
.card:has(.card__image) {
padding-top: 0;
}
と書けます。
Gridの行構成を変えることもできます。
.card:has(.card__image) {
display: grid;
grid-template-rows: auto 1fr auto;
}
ポイントは「:has()ならclassを減らせる」ことではありません。
条件にしたい情報が、すでにHTMLの構造として存在しているか。
存在しているなら:has()が候補になります。
02|チェックされた項目の親カードを強調する
チェックボックスやラジオボタンをカード型UIにするときにも便利です。
<label class="plan">
<input type="radio" name="plan" value="light">
<span class="plan__name">LIGHT</span>
<span class="plan__price">198,000円〜</span>
</label>
選択状態は、すでにinput:checkedとしてブラウザが持っています。
.plan:has(input:checked) {
border-color: #316fcf;
background: #f3f7ff;
}
これだけで、
中のinputが:checkedなら.planを強調する
という条件を書けます。
JavaScriptで.is-selectedのような見た目用classを別途付ける必要がなく、実際の選択状態と見た目もずれにくくなります。
ただし、料金計算やデータ取得などの処理までCSSへ任せるものではありません。
:has()が担当するのは、すでにある状態を見てスタイルを変える部分です。
03|入力エラーがあるフォームグループを親ごと変える
フォームでは、入力欄だけでなくラベルや補足文を含むグループ全体をエラー表示にしたいことがあります。
<div class="form-group">
<label for="email">メールアドレス</label>
<input id="email" type="email" required>
<p class="form-group__help">メールアドレスを入力してください。</p>
</div>
inputの状態を条件に親を選べます。
.form-group:has(input:user-invalid) {
border-color: #c62828;
}
:invalidでも条件にはできます。
.form-group:has(input:invalid) {
border-color: #c62828;
}
ただし、:invalidは入力前から条件を満たす場合があります。
たとえばrequiredが付いた空の入力欄は、まだ操作していなくてもinvalidになり得ます。
ユーザーが操作したあとに不正な値として扱われている状態を見たい場合は、:user-invalidが選択肢になります。
.form-group:has(input:user-invalid) .form-group__help {
color: #c62828;
}
:has()はフォームバリデーションそのものを代替するものではありません。
ブラウザやアプリケーションがすでに持っている状態を、親側の見た目へ反映するために使います。
04|aria-currentがあるリンクの親を現在地表示にする
アクセシビリティのために付けている属性を、見た目にも利用できます。
<nav class="global-nav" aria-label="メインナビゲーション">
<ul>
<li class="global-nav__item">
<a href="/">HOME</a>
</li>
<li class="global-nav__item">
<a href="/service/" aria-current="page">SERVICE</a>
</li>
</ul>
</nav>
リンク自身だけを装飾するなら、
.global-nav__item a[aria-current="page"] {
font-weight: 700;
}
で十分です。
ただ、親のliへ背景や下線を付けたい場合は:has()が使えます。
.global-nav__item:has(> a[aria-current="page"]) {
background: #f3f7ff;
}
これなら現在地表示のためだけに、
<li class="global-nav__item is-current">
と別のclassを追加する必要がありません。
aria-currentはCSSのために付ける属性ではありません。
まずアクセシビリティ上の意味として適切に付け、すでに存在する状態をCSSでも利用するという順番が重要です。
05|後ろに画像が続く見出しだけ余白を変える
ここから「:has()=親を選ぶもの」だけではないことが分かりやすくなります。
記事本文に、
<h2>見出し</h2>
<figure>
<img src="image.jpg" alt="">
</figure>
という並びと、
<h2>見出し</h2>
<p>本文が入ります。</p>
という並びが混在しているとします。
見出しの直後が画像のときだけ余白を狭くするなら、
h2:has(+ figure) {
margin-bottom: 1rem;
}
と書けます。
直後にfigureがあるh2
を選んでいます。
figureはh2の子要素ではなく兄弟要素です。
つまり、:has()は「子要素から親を選ぶためだけのセレクタ」ではありません。
:has()の中には相対セレクタを書けるため、
:has(+ figure)
なら直後の兄弟、
:has(~ figure)
なら後続する兄弟関係も条件にできます。
検索では「CSSの親セレクタ」として知ることが多いですが、実務ではこの考え方まで広げると使える場面が増えます。
06|子要素の数によってレイアウトを変える
:has()と:nth-child()を組み合わせると、子要素の個数も条件にできます。
たとえばカード一覧が4枚以上あるときだけ2列にしたいとします。
<div class="cards">
<article class="card">01</article>
<article class="card">02</article>
<article class="card">03</article>
<article class="card">04</article>
</div>
.cards:has(> :nth-child(4)) {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
:nth-child(4)が存在するということは、少なくとも4個以上の直接の子要素があるということです。
そのため、
:has(> :nth-child(4))
で「4個以上」を条件にできます。
「ちょうど4個」なら、4個目が最後の子要素でもあることを条件にします。
.cards:has(> :nth-child(4):last-child) {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
便利ですが、このあたりからセレクタは少し読みづらくなります。
条件がさらに増えるなら、状態classやJavaScriptの方が分かりやすい場合もあります。
:has()で書けることと、:has()で書くべきことは同じではありません。
07|モーダルが開いているときページ側のスタイルを変える
最後は、ページ全体の状態を見る例です。
dialog要素を、
dialog.showModal();
でモーダルとして表示すると、そのdialogは:modalの条件に一致します。
そこで、
html:has(dialog:modal) {
overflow: hidden;
}
と書くと、
ページ内にモーダル状態のdialogが存在するとき
を条件に、html側のスタイルを変えられます。
[open]と:modalは同じではない
似た書き方に、
html:has(dialog[open]) {
overflow: hidden;
}
があります。
ただし、dialog[open]とdialog:modalは同じ意味ではありません。
[open]は開いているdialogを条件にしますが、:modalはモーダル状態の要素を条件にします。
たとえばdialog.show()で開いた非モーダルのdialogはopen状態になりますが、:modalには一致しません。
そのため、モーダルダイアログが開いている状態を条件にするなら、
dialog:modal
の方が意図を表しやすくなります。
なお、overflow: hidden;だけですべての端末のスクロール制御が完成するとは限りません。
スクロール位置の保持やモバイル特有の挙動まで管理する必要がある場合は、JavaScript側での制御も検討します。
ここで重要なのは、
モーダルという子孫側のUI状態を、ページのルート要素から条件にできる
という:has()の使い方です。
:has()を使うときに覚えておきたいこと
JavaScriptが不要になるわけではない
:has()と相性がよいのは、次のような処理です。
- DOM上にすでに存在する要素を見る
:checkedなどブラウザが持つ状態を見る- 属性の有無を見る
- 要素同士の位置関係を見る
- その結果によって見た目を変える
一方、
- 要素の追加・削除
- APIからのデータ取得
- 状態の保存
- URLや履歴の変更
- フォーカス管理
aria-expandedなど状態属性の更新- 複数UIの同期
- ビジネスロジック
などはJavaScriptの役割です。
CSSから見える状態を使って見た目だけ変えるならCSS。アプリケーションとして管理すべき状態ならJavaScript。
詳細度には注意する
:has()では、引数に含まれるセレクタの詳細度が影響します。
.component:has(.content .item.is-active) {
}
のように条件を詰め込みすぎると、あとから上書きしにくくなる場合があります。
実務では、
- 条件を複雑にしすぎない
- コンポーネントの範囲を狭くする
- 読みやすさを優先する
- 状態classの方が明確なら無理に置き換えない
くらいで考えると扱いやすくなります。
:has()の中へ:has()は入れられない
:has()はネストできません。
/* NG */
.parent:has(.child:has(.grandchild)) {
}
条件がここまで複雑になったら、1本のセレクタへ押し込まず、HTML構造や状態class、JavaScript側での管理も含めて見直した方が分かりやすくなります。
2026年なら通常の選択肢に入れられる
:has()は現在、主要なモダンブラウザで利用できます。
「新しすぎるから原則使わない」機能ではなく、案件の対応ブラウザを確認したうえで通常の選択肢に入れられる
と考えやすくなっています。
ただし、WebViewや古い端末まで含め、実際の案件で求められる対応環境は確認しておきます。
何でも:has()へ置き換えない
たとえば、
<article class="card card--featured">
のcard--featuredが「重要記事」「おすすめ商品」などコンテンツ自体の意味を表すなら、そのclassを残した方が分かりやすいでしょう。
一方、
画像が入っているから余白だけ変えたい
という条件なら、
.card:has(.card__image) {
}
の方が状態の重複を減らせます。
判断するときは、
この状態はHTMLやアプリケーションとして名前を付けて管理する意味があるか?
を見ると分かりやすいです。
意味があるならclass。
HTMLにすでに答えがあり、見た目だけ変えたいなら:has()。
このくらいの使い分けがちょうどよいと思います。
「昔はこうしていたけれど、今はCSSだけで書ける」という変化については、
【2026年版】Web制作歴22年。「昔は大変だった」と感じる便利なCSS 10選
でもまとめています。
まとめ|:has()は「親セレクタ」だけで覚えない
今回の7つの例を整理すると、次のようになります。
01 子要素の存在を見る
.card:has(.card__image)
02 子要素の選択状態を見る
.plan:has(input:checked)
03 子要素の入力状態を見る
.form-group:has(input:user-invalid)
04 属性を持つ子要素を見る
.global-nav__item:has(a[aria-current="page"])
05 兄弟要素との関係を見る
h2:has(+ figure)
06 子要素の数を見る
.cards:has(> :nth-child(4))
07 UIの状態を見る
html:has(dialog:modal)
最初は、
子要素を条件に親を選べるCSS
として理解して問題ありません。
ただ、:has()はより広く、
中や周囲に条件を満たす要素があるかを見て、現在の要素を選ぶ
ためのセレクタです。
そして、できるからといって何でも:has()へ置き換える必要はありません。
- HTMLに意味のある状態ならclass
- JavaScriptで管理すべき状態ならJavaScript
- HTMLにすでに答えがあり、見た目だけ変えたいなら
:has()
この判断ができれば、JavaScriptで見た目用classを追加していた処理の一部を、自然にCSSへ戻せるようになります。
新しいCSSを覚える目的は、単にコードを短くすることではありません。
今のブラウザでできることを知ったうえで、その案件に合う一番分かりやすい方法を選ぶこと。
:has()も、その選択肢の一つとして使っていきたい機能です。
Contact
CSSとJavaScriptの役割まで整理して、
あとで困りにくいWebサイトを考えます
KU NEL WEBLEでは、見た目だけを合わせるのではなく、HTMLの意味・CSSの役割・JavaScriptで管理すべき状態を整理しながら実装しています。
こんなご相談も大丈夫です
「動いてはいるけれど、もっとシンプルにできそう」という段階でも大丈夫です。既存の実装を確認しながら、無理に新しい書き方へ置き換えず整理します。