「子要素の状態に応じて、親要素のスタイルを変えたい」
Web開発をしていると、何度もぶつかるこの要望。これまで CSS だけでは不可能で、JavaScript でクラスを付け外しするのが定番でした。
それが CSS :has() 擬似クラスの登場で、ついに CSS 単独で「親要素の選択」が可能になりました。すべての主要ブラウザでサポートされ、本番投入できるレベルに到達しています。
この記事では、:has() の基本構文から、現場ですぐ使える 5つの実用パターン までを、コード例つきで解説します。
この記事のゴール
:has() の構文を理解し、これまで JavaScript で書いていた処理を CSS に置き換える具体的な引き出しを5つ手に入れることです。
:has() 擬似クラスとは?
:has() は 「特定の子要素・子孫要素を持つ親要素」 を選択するための CSS 擬似クラスです。
/* 「画像を含む article」だけにスタイルを当てる */
article:has(img) {
padding: 24px;
background: #f8fafc;
}
この記述は 「img を子孫に持つ article」 という意味になります。article 自体が選択対象であり、img ではない点がポイントです。
なぜ「親セレクタ」と呼ばれるのか
従来の CSS セレクタは 「左から右」 にしか辿れませんでした。
article img→ article の中の imgarticle > img→ article の直下の img
しかし :has() を使うと、子から親をさかのぼって選択できます。これが「親セレクタ」と呼ばれる所以です。
基本構文
:has() の引数には、通常の CSS セレクタを渡せます。
/* 直下の子に img があるカード */
.card:has(> img) { ... }
/* チェックされたチェックボックスを含む label */
label:has(input:checked) { ... }
/* エラー状態の input を持つフォーム */
form:has(input:invalid) { ... }
/* img も video も持たない article */
article:not(:has(img, video)) { ... }
セレクタリスト(カンマ区切り)も渡せるので、「画像か動画のいずれかを持つ」 といった条件も書けます。
パターン1: 画像を含むカードだけレイアウトを変える
ECサイトや記事一覧でよくある要望です。
/* デフォルトのカード */
.card {
display: block;
padding: 16px;
}
/* 画像を含むカードは横並びに */
.card:has(img) {
display: grid;
grid-template-columns: 120px 1fr;
gap: 16px;
}
これまでなら HTML 側で .card--with-image のようなクラスを付ける必要がありました。:has() なら HTML はそのままで、CSS だけで条件分岐できます。
パターン2: チェック状態で親要素を変化させる
「選択中のオプションだけ強調する」UI を JavaScript なしで実現できます。
<label class="option">
<input type="checkbox" />
<span>プレミアムプラン</span>
</label>
.option {
border: 2px solid #e5e7eb;
padding: 12px 16px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
}
/* チェックされたら label 全体を強調 */
.option:has(input:checked) {
border-color: #0ea5e9;
background: #f0f9ff;
color: #0c4a6e;
}
ラジオボタンやチェックボックスの 「選択中のスタイル」 を、JavaScript なしでスマートに表現できます。
パターン3: 子要素のエラー状態をフォームに伝える
フォーム内のどこかにエラーがあったら、フォーム全体のヘッダーを赤くするといった処理も簡単です。
form {
border: 2px solid #e5e7eb;
padding: 24px;
border-radius: 8px;
}
/* 中に invalid な入力があれば赤枠に */
form:has(input:invalid, textarea:invalid) {
border-color: #ef4444;
}
/* さらに、エラー時だけ表示する警告メッセージ */
.form-error-banner {
display: none;
}
form:has(:invalid) .form-error-banner {
display: block;
}
:has() と他のセレクタを 組み合わせることで、「親に条件があるとき、別の子孫を表示する」といった複雑な要件もスッキリ書けます。
パターン4: 子要素の数でレイアウトを切り替える
CSS 単独で 「子要素が3つ以上なら段組み、それ未満なら1列」 のような分岐ができます。
.gallery {
display: flex;
flex-direction: column;
gap: 12px;
}
/* 子要素を3つ以上持つときだけグリッド表示 */
.gallery:has(> :nth-child(3)) {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
:nth-child(3) という子セレクタを :has() の中で使うことで、「3番目の子が存在する=子が3個以上」 という条件を表現しています。
パターン5: 現在地に応じて親メニューをハイライト
ナビゲーションの 「現在のページを含むカテゴリ」 をハイライトしたいときに便利です。
<li class="menu-group">
<span>製品</span>
<ul>
<li><a href="/product/a/">プロダクトA</a></li>
<li><a href="/product/b/" aria-current="page">プロダクトB</a></li>
</ul>
</li>
/* aria-current を持つリンクを含むメニューグループ */
.menu-group:has(a[aria-current="page"]) {
background: #f0f9ff;
font-weight: 600;
}
これまでサーバー側で「現在地クラス」を出力していた処理が、HTML の aria 属性だけで完結します。アクセシビリティと装飾を同時に解決できる、エレガントな書き方です。
ブラウザ対応について
:has() は 2023年12月にすべての主要ブラウザ(Chrome / Edge / Safari / Firefox)で安定サポートに到達し、Baseline 2023 として認定されています。
2026年現在、ほぼすべてのユーザーが利用可能と考えてよいでしょう。心配な場合は、機能検出で安全にフォールバックできます。
/* :has() をサポートする環境でだけ適用 */
@supports selector(:has(*)) {
.card:has(img) {
display: grid;
grid-template-columns: 120px 1fr;
}
}
つまずきやすいポイント
⚠ ここは要注意
:has() はネスト不可です。:has(:has(...)) のように入れ子で使うと無効になります。
- ネスト不可:
:has()の中に:has()を書くことはできません :has()の中で:has()以外の擬似要素は使えない:::beforeや::afterなどはNG- パフォーマンス: 大量の DOM 要素に複雑な
:has()を当てると重くなることがあります。プロファイラで確認しながら使うのが安全です - セレクタの優先度:
:has()自体には特異性(specificity)がなく、引数の中で最も特異性が高いセレクタが採用されます
まとめ
CSS :has() 擬似クラスを使うと、以下が CSS 単独で実現できます。
- ✅ 子要素の有無に応じて親のレイアウトを変える
- ✅ チェック・フォーカス・エラー状態を親に伝える
- ✅ 子要素の数でデザインを切り替える
- ✅ 現在地に応じてナビをハイライトする
- ✅ HTML を変えずに、見た目だけ条件分岐できる
これまで JavaScript で 「className を付け外しする」 ことで実現していた多くの処理が、:has() で 宣言的・状態同期型に書き直せます。コード量が減り、状態と表示の同期バグも起きにくくなる嬉しい変化です。
ぜひ今日から実プロジェクトに取り入れてみてください。
💡 次のステップ:
:has()と相性が良い CSS の新機能には コンテナクエリ(@container) や:is()/:where()があります。これらを組み合わせると、より柔軟で読みやすいスタイル設計が可能になります。