CSSの:has()は、指定した子要素や状態を持つ要素を選択できる疑似クラスです。これまでJavaScriptでクラスを付けなければ難しかった「子要素を見て親を変える」処理を、CSSだけで書けるようになります。
この記事では、:has()の基本構文と注意点を整理したうえで、フォーム、カード、ナビゲーション、一覧レイアウトなどで使える実践サンプルを20個紹介します。各コードは必要な部分だけ抜き出して、そのまま試せる形にしています。
説明
:has()とは?
:has()は、括弧内の条件に一致する要素が存在するとき、基準となる要素を選択する関係疑似クラス(relational pseudo-class)です。
/* imgを含む.cardを選択 */
.card:has(img) {
border-color: #2384ff;
}
通常のCSSは親から子へ向かって要素を選びます。一方、:has()を使うと子や後続要素の状態を条件にできるため、「CSSの親セレクタ」と呼ばれることもあります。ただし、実際には親だけでなく兄弟要素や複数条件も扱えます。
基本構文
基準となる要素:has(条件) {
/* 条件に一致した場合のスタイル */
}
.card:has(img):画像を含むカードlabel:has(input:checked):チェック済みinputを含むlabelh2:has(+ p):直後にpがあるh2form:has(:invalid):入力エラーを含むform
:has()を使う前に知っておきたいポイント
詳細度は括弧内のセレクタに影響される
:has()自体が一律の詳細度を持つのではなく、引数に含まれるセレクタのうち最も高い詳細度が採用されます。IDセレクタを混ぜると上書きしにくくなるため、クラス中心で設計するのが安全です。
:has()の入れ子はできない
/* NG */
.card:has(.media:has(img)) {
/* ... */
}
複雑な条件は、:has(.media img)のように1つの:has()内へまとめます。
対応していない環境向けには@supportsを使う
@supports selector(.card:has(img)) {
.card:has(img) {
border-color: #2384ff;
}
}
現在の主要ブラウザでは利用できますが、古いブラウザや組み込みブラウザまで対象にする場合は、@supports selector()で段階的に適用すると安全です。
実装例
ここからは、実務で使いやすい:has()のサンプルを20個紹介します。HTMLとCSSは必要な部分だけを抜き出しているため、そのままコピーして調整できます。
1. 画像を含むカードだけレイアウトを変更する
<article class="card">
<img src="photo.jpg" alt="">
<h2>記事タイトル</h2>
</article>
.card:has(img) {
display: grid;
grid-template-columns: 160px 1fr;
gap: 20px;
}
画像があるカードだけ横並びにできます。画像なしカード用の余白調整を別クラスで管理する必要がありません。
2. 画像がないカードにプレースホルダーを表示する
.card:not(:has(img))::before {
content: "NO IMAGE";
display: grid;
place-items: center;
min-height: 160px;
background: #e6e6e6;
color: #757575;
}
:not()と組み合わせれば「持っていない場合」も選択できます。
3. チェック済みのラベルを強調する
<label class="choice">
<input type="checkbox">
CSS
</label>
.choice:has(input:checked) {
border-color: #2384ff;
background: #eaf3ff;
color: #111;
}
選択中のラベルへクラスを付けるJavaScriptが不要になります。
4. フォーカス中の入力欄を含む項目を強調する
.field:has(input:focus-visible),
.field:has(textarea:focus-visible) {
outline: 3px solid #2384ff;
outline-offset: 4px;
}
入力欄だけでなく、ラベルや説明文を含むフィールド全体を視覚的に示せます。
5. 入力エラーがあるフォームを表示する
form:has(input:user-invalid) .form-error {
display: block;
}
.form-error {
display: none;
color: #c5163a;
}
:user-invalidを使うと、ユーザー操作後に無効となった入力を条件にできます。
6. 必須項目を含むフィールドに「必須」を付ける
.field:has(:required) .field-label::after {
content: "必須";
margin-left: 8px;
padding: 2px 6px;
background: #ff2f63;
color: #fff;
font-size: 12px;
}
HTMLのrequired属性を表示にも反映でき、マークアップと見た目の食い違いを防げます。
7. 入力済みの項目だけ完了状態にする
.field:has(input:not(:placeholder-shown):valid) {
border-left: 4px solid #00a88f;
}
入力済みかつ妥当な値を持つフィールドに完了状態を付ける例です。空の入力を判定するため、HTML側にはplaceholderが必要です。
8. 無効なボタンを含む操作エリアを薄くする
.actions:has(button:disabled) {
opacity: .55;
}
.actions:has(button:not(:disabled)) {
opacity: 1;
}
送信条件を満たしていない操作エリア全体へ状態を反映できます。
9. アクティブなリンクを含むナビ項目を強調する
.global-nav li:has(a[aria-current="page"]) {
background: #111;
}
.global-nav li:has(a[aria-current="page"]) a {
color: #fff;
}
aria-current="page"を基準にすれば、アクセシビリティと現在地表示を同じ情報から管理できます。
10. サブメニューを持つ項目に矢印を付ける
.menu-item:has(.submenu) > a::after {
content: "▼";
margin-left: 8px;
font-size: .7em;
}
サブメニューの有無に応じた専用クラスが不要になります。
11. ホバーまたはフォーカス中のカード以外を薄くする
.card-list:has(.card:hover) .card:not(:hover),
.card-list:has(.card:focus-within) .card:not(:focus-within) {
opacity: .45;
}
一覧のどれかが操作中であることを親側で検知し、他のカードだけを抑えます。キーボード操作も考慮して:focus-withinを併記しています。
12. リンクを含むカード全体をクリック可能に見せる
.card:has(.card-link) {
position: relative;
cursor: pointer;
}
.card:has(.card-link:hover) {
translate: 0 -4px;
box-shadow: 0 12px 30px rgb(0 0 0 / 15%);
}
カード内リンクのホバー状態をカード全体へ反映できます。実際のクリック範囲を広げる場合は、リンク要素の配置も別途設計してください。
13. 見出しの直後に本文がある場合だけ余白を調整する
h2:has(+ p) {
margin-bottom: 12px;
}
h2:has(+ .lead) {
margin-bottom: 24px;
}
+は直後の兄弟要素を表します。次に続く内容に応じて見出しの余白を変えられます。
14. 見出しを持つセクションだけ区切り線を付ける
section:has(> h2) {
padding-top: 48px;
border-top: 1px solid #d8d8d8;
}
>を付けると、直下の見出しだけを条件にできます。
15. ボタンが2個以上ある場合だけ横並びにする
.button-group:has(.button:nth-child(2)) {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
2番目のボタンが存在することを利用した件数判定です。1個だけなら通常表示、2個以上なら横並びにできます。
16. 要素が奇数個のグリッドで最後のカードを横幅いっぱいにする
.grid:has(> .card:last-child:nth-child(odd))
> .card:last-child {
grid-column: 1 / -1;
}
2列グリッドで最後に1枚だけ余る場合の調整に使えます。
17. 動画を含む記事だけ幅を広げる
.article:has(video, iframe[src*="youtube.com"]) {
max-width: 1100px;
}
.article:not(:has(video, iframe)) {
max-width: 760px;
}
:has()にはカンマ区切りで複数条件を指定できます。
18. 売り切れ商品を含む商品一覧に案内を表示する
.product-list:has(.sold-out)::before {
content: "一部の商品は売り切れです";
display: block;
margin-bottom: 16px;
padding: 12px;
background: #fff4d6;
color: #5d4300;
}
子要素の状態を一覧全体のメッセージへ反映する典型例です。
19. 選択された行を含むテーブルを強調する
table:has(input[type="checkbox"]:checked) {
border-color: #2384ff;
}
tr:has(input[type="checkbox"]:checked) {
background: #eaf3ff;
}
一括操作の対象行を分かりやすくできます。チェックボックスと行の状態がCSSだけで同期します。
20. CSSだけでテーマを切り替える
<body>
<label>
<input class="theme-switch" type="checkbox">
ダークモード
</label>
<main>コンテンツ</main>
</body>
body:has(.theme-switch:checked) {
color-scheme: dark;
background: #111;
color: #fff;
}
スイッチがbody内にあれば、その状態をページ全体へ反映できます。実案件ではユーザー設定の保存にJavaScriptが必要ですが、見た目の切り替え自体はCSSだけで実装できます。
よくある失敗
:has()は便利ですが、探索範囲や操作方法を考えずに使うと、保守性やアクセシビリティを損なうことがあります。特に注意したい失敗例を確認します。
広すぎる探索範囲を避ける
body:has(...)や*:has(...)のような広範囲の指定を大量に使うと、変更時にブラウザが確認する範囲も広がります。まずコンポーネントのクラスへ絞り、条件もできるだけ具体的に書くのが基本です。
/* 範囲が広い */
body:has(.error) { /* ... */ }
/* コンポーネントへ限定 */
.contact-form:has(.field-error) { /* ... */ }
重要な機能をCSSだけに依存させない
:has()は表示やフィードバックに便利ですが、フォーム検証、データ保存、アクセス制御などの機能を担うものではありません。HTMLの属性、サーバー側検証、必要なJavaScriptと組み合わせて使います。
キーボード操作も一緒に設計する
:hoverだけを条件にすると、キーボードやタッチ操作では同じ体験になりません。操作状態には:focus-visibleや:focus-withinも併用しましょう。
応用
ブラウザ対応と段階的な適用
:has()は現在のChrome、Edge、Safari、Firefoxなど主要ブラウザで利用できます。一方、古いブラウザや更新されないWebViewを対象に含む場合は、実機要件を確認してください。
対応外でもコンテンツを読める基本レイアウトを先に作り、@supports selector(:has(*))内で装飾や利便性を追加するプログレッシブエンハンスメントが扱いやすい方法です。
参考資料
まとめ
:has()を使うと、子要素や兄弟要素の有無、フォームの状態、リスト内の選択状態などを条件にして、基準となる要素へスタイルを適用できます。
- 画像の有無でカードレイアウトを変える
- 入力状態をフィールドやフォーム全体へ反映する
- 現在地やサブメニューを持つナビ項目を選ぶ
- 一覧の件数や子要素の状態でレイアウトを調整する
- JavaScriptによる表示用クラスの付け替えを減らす
まずはカードやフォームなど、探索範囲が限定されたコンポーネントから導入すると安全です。従来はマークアップやJavaScript側で吸収していた条件分岐を、より宣言的に整理できます。

