画像があるカードにだけhas-imageクラスを付ける。チェックが入ったら親へis-selectedを付ける。こうした「子の状態を見て、親の見た目を変える」処理は、CSSの:has()で書ける場合があります。
例えば.card:has(> img)は、直下にimgがある.cardを選びます。画像そのものを選ぶ.card > imgとは、スタイルが付く場所が違います。まずこの違いを押さえると、親要素への条件指定を読みやすくなります。
:has()の前が選ぶ要素、括弧の中が条件になる
| セレクター | 選ばれるもの |
|---|---|
| .card > img | .cardの直下にあるimg |
| .card:has(> img) | 直下にimgを持つ.card |
| .card:has(img) | 子孫のどこかにimgを持つ.card |
| h2:has(+ p) | 直後の兄弟要素がpであるh2 |
>を使うと直下の子に絞れます。小さなアイコンが深い場所にあるだけでカード全体を画像付きにしたくないなら、.card:has(img)より.card:has(> img)が意図に近くなります。
また、:has()は「親専用」の構文ではありません。+のような兄弟の関係も条件にできます。Selectors Level 4の:has()の定義は、基準になる要素から見た相対セレクターの条件として説明しています。
以下では、画像の有無、チェック状態、キーボードでの入力位置という3つの条件を試します。クリックや入力のたびにclassを同期するJavaScriptは、このデモにはありません。
HTMLとCSSを3ファイルに分けて試す
同じフォルダーへindex.html、base.css、enhance.cssを保存し、index.htmlをブラウザで開いてください。base.cssは基本表示、enhance.cssは:has()を使った追加の見た目です。
HTML全体は次のとおりです。カードの画像はデモ用の装飾なので、altを空にしています。実際の画像が内容を伝える場合は、その内容に合う代替テキストを入れます。
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>:has()の3つの例</title>
<link rel="stylesheet" href="base.css">
<link rel="stylesheet" href="enhance.css">
</head>
<body>
<main>
<h1>:has()の3つの例</h1>
<section aria-labelledby="cards-title">
<h2 id="cards-title">画像のあるカード</h2>
<article class="card" id="with-image">
<img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Crect width='120' height='120' rx='16' fill='%23b8dcca'/%3E%3Ccircle cx='60' cy='60' r='30' fill='%23295642'/%3E%3C/svg%3E" alt="" width="120" height="120">
<div><h3>画像がある記事</h3><p>直下にimgがあると、画像と本文が横に並びます。</p></div>
</article>
<article class="card" id="without-image">
<div><h3>画像がない記事</h3><p>本文だけの1列になります。</p></div>
</article>
</section>
<section aria-labelledby="settings-title">
<h2 id="settings-title">チェックと追加の設定</h2>
<fieldset class="settings">
<legend>表示の設定</legend>
<label class="toggle-row"><input id="show-advanced" type="checkbox"> 追加の設定を使う</label>
<div class="advanced">
<label for="page-size">1ページの表示件数</label>
<select id="page-size"><option>10件</option><option>20件</option></select>
</div>
</fieldset>
</section>
<section aria-labelledby="focus-title">
<h2 id="focus-title">入力中の行を強調する</h2>
<div class="form-row"><label for="display-name">表示名</label><input id="display-name" type="text" autocomplete="nickname"></div>
<div class="form-row"><label for="bio">ひとこと</label><input id="bio" type="text"></div>
</section>
</main>
</body>
</html>
base.cssは、追加の条件指定がなくても内容を読める形にします。カードは1列、追加設定は表示した状態です。動作しない切り替えだけが残らないよう、チェックボックスの行は最初は非表示にします。
* { box-sizing: border-box; }
body { margin: 0; color: #20352d; background: #f4f7f5; font-family: system-ui, sans-serif; line-height: 1.7; }
main { max-width: 760px; margin: 0 auto; padding: 24px 20px; }
section { margin-block: 32px; }
h1 { font-size: 1.7rem; }
h2 { font-size: 1.3rem; }
h3, p { margin: 0 0 8px; }
.card { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; padding: 20px; margin-block: 16px; background: white; border: 1px solid #adc2b7; border-radius: 12px; }
.card > img { width: 100%; max-width: 120px; height: auto; }
.card > div { min-width: 0; }
.settings { min-width: 0; margin: 0; padding: 20px; border: 1px solid #adc2b7; }
.toggle-row { display: none; }
.advanced { display: grid; gap: 8px; margin-top: 12px; }
.form-row { display: grid; gap: 8px; padding: 16px; margin-block: 12px; border: 1px solid #adc2b7; border-radius: 8px; }
input, select { font: inherit; }
input[type="text"], select { width: 100%; min-width: 0; padding: 8px; }
input:focus-visible, select:focus-visible { outline: 3px solid #216c4b; outline-offset: 3px; }
enhance.cssでは、@supports selector(:has(*))の中へ条件付きの指定をまとめます。対応しているブラウザで、画像付きカードの2列表示と、チェックによる追加設定の開閉を有効にします。
@supports selector(:has(*)) {
.card:has(> img) { grid-template-columns: minmax(64px, 100px) minmax(0, 1fr); }
.toggle-row { display: block; }
.settings:has(> .toggle-row > input:not(:checked)) > .advanced { display: none; }
.settings:has(> .toggle-row > input:checked) { border-color: #216c4b; }
.form-row:has(> input:focus-visible) { background: #e0f2e8; }
}
チェックボックスの行を表示する指定と、追加欄を隠す指定を同じ@supportsに入れるのが要点です。最初から追加欄を隠すだけのCSSを書くと、:has()を利用できない環境で表示へ戻す手段がなくなってしまいます。
3つの条件を変えて、どこが変化するか確かめる
画像を取り除くと、カードが1列へ戻る
画像ありのカードでは、.card:has(> img)が一致して2列になります。ブラウザの開発者ツールで直下のimgを取り除くと、base.cssの1列表示へ戻ります。検証でも、imgの削除と再追加で1列・2列が切り替わりました。
ここで調べているのはDOMにimgが存在することです。画像の読み込み成功や、ファイルが壊れていないことではありません。読み込み失敗を検出して代替表示へ変える必要がある場合は、その状態を別に扱います。
また、画像にCSSでdisplay:noneを付けても、DOMから消えたことにはなりません。画像の有無と、表示されているかどうかを同じ条件だと思わないようにします。
チェックボックスはSpaceでも操作できる
Tabキーで「追加の設定を使う」へ移り、Spaceキーでチェックを切り替えると、表示件数の欄が現れます。もう一度切り替えると隠れます。チェックボックス本来の操作を使うので、クリック専用のdivを用意する必要はありません。
条件は.settings:has(> .toggle-row > input:checked)です。.settingsの直下にあるラベル、その直下のinputという位置を指定しています。別の場所に追加されたチェックボックスへ、意図せず反応するのを避けるためです。
このデモの追加欄は、見た目だけを切り替える例です。display:noneにしても、入力値が消えたりdisabledになったりはしません。 実際のフォームへ組み込むときは、送信する値、必須項目、無効化する条件を別に設計します。ここでは送信や保存の処理は実装していません。
入力欄のフォーカスに合わせて、親の行を強調する
.form-row:has(> input:focus-visible)は、直下のinputがfocus-visibleに一致した行の背景を変えます。キーボードで表示名からひとことへ移ると、背景の強調も次の行へ移ります。
親の背景だけに頼らず、input自体のoutlineも残しています。:has()の追加CSSを使わなくても、フォーカスの位置を入力欄で確認できる形です。
:focus-visibleは「キーボードのときだけ」と固定されているわけではなく、フォーカス表示が必要かをブラウザが判断します。どんなフォーカスでも親を強調したいだけなら、.form-row:focus-withinの方が単純です。今回は直下のinputとfocus-visibleという条件を選ぶため、:has()で書いています。
JavaScriptで持つ状態と、CSSから読める状態を分ける
| やりたいこと | :has()で扱える条件 | 別に必要になる処理 |
|---|---|---|
| 画像付きカードの並び替え | 直下のimgの存在 | 画像の取得・失敗時のデータ処理 |
| チェックに応じた親の強調や欄の表示 | inputの:checked | 値の保存、送信項目やdisabledの更新 |
| 入力中の行を強調 | inputの:focus-visible | 入力内容の検証やエラー文の生成 |
| 非同期処理が終わったら表示を変える | 処理結果がclassや属性に反映されていれば読める | 通信・結果の判定・状態の反映 |
すでにJavaScriptでDOMを作ったりチェック状態を変えたりしている場合も、その結果に応じた見た目をCSSへ置けます。今回の検証では、イベントを発火せずにcheckedプロパティをfalseへ変えても、追加欄の表示が切り替わりました。
一方、ボタンでパネルを開閉し、aria-expandedやhiddenを同期するUIを、そのまま見た目だけの:has()へ置き換えると意味が変わります。状態と属性を更新する必要がある場合は、JavaScriptのclassList・style操作のように、元の状態管理を維持します。
CSSの条件が増えるほど、HTMLの構造とのつながりも強くなります。後でラベルやラッパーを追加したら、>の関係が変わっていないかも見直してください。
効かないときは、対応・構造・詳細度を順に見る
まず利用するブラウザで構文が対応しているか、@supportsの内側が有効かを確認します。対応状況はMDNの:has()対応表で確認できます。ブラウザ名だけでなく、利用対象のバージョンも合わせます。
次に、条件が実際のDOMと合っているかを見ます。imgがpictureの中へ移った場合、.card:has(> img)には一致しなくなります。その構造に合わせて、何が直下の子なのかを確認します。
さらに、:has()の括弧内の詳細度も影響します。.card:has(> img)は0-1-1ですが、引数にIDを加えると強さが変わります。詳細度を抑えたい場合は、.card:where(:has(> img))のように:where()で条件部分を包む方法があります。詳しい計算は、CSS詳細度の考え方と合わせて確認してください。
:has()の中へさらに:has()を入れる書き方はできません。条件を追加したいときは、構造に応じて引数や外側のセレクターを整理します。また、bodyや*へ広い条件をまとめるより、今回の.cardや.settingsのように、関係する部品へ範囲を絞ると、何が影響するかを追いやすくなります。
今回のデモはChromium 149で幅390px・1440pxを確認し、キーボード操作、画像の削除・追加、checkedの変更が反映されました。enhance.cssを読み込まずJavaScriptも無効にした条件では、カードは1列、追加設定は表示されたままで、selectと入力欄を操作できました。これは基本表示の確認であり、古いブラウザそのものを実行した結果ではありません。
導入時は、条件が一致したときの見た目だけでなく、条件が消えたときと追加CSSが使えないときも確認します。表示のためだけに二重管理していたclassを減らしつつ、入力や送信の役割は残す。その区別ができると、:has()を小さな部品から取り入れやすくなります。