この記事は『レスポンシブは線から面へ、そして空間へ』の続きになります。先にそちらをご覧ください。
理論上はすべての要素を、任意の子要素が制御できる:has()疑似クラス
| 2026/06/06
:has()疑似クラスはCSSのIF文
よく言われるのは、:has()疑似クラスはCSSのIF文。まさにその通りで、条件を設定しその条件を満たせば子孫の要素に対してスタイルを制御することが可能になる。条件には、特定の子要素やクラス、IDなどを含むか含まないかなどは当然だけど、CSS の状態も含まれ、:hover,:activeなどに加え :focus, :checked, などのフォーム系、:target,:empty,やdetails系の:openなど多くあり、簡単なIF文なら:has()疑似クラスがあればJS使う必要はなくなった。特にフォーム系は昔はフォームパーツにフォーカスしたり、チェックしたりしても親に伝えるにはJSが必要だったけど、今は:has()疑似クラスを使えばCSSだけで判定できるようになった。
さらに厳密にいうと、子が親を制御できるようになったというより、子が先祖だけでなく、枝分かれの兄弟やいとこまで制御できるようになったという方が正確といえる。bodyを:has()疑似クラスの親にすれば理論上はすべての要素が、任意の子要素の制御対象になるからね。
:has()疑似クラスの簡単な紹介
まだ:has()疑似クラスをよく知らないという方のために、MDNで紹介されている内容を引用して紹介してみよう。
たとえば、下記の様なHTMLがある。
<header>
<h1>とある王国の物語</h1>
</header>
<section>
<article>領民のとるべき道は</article>
</section>
通常、こういう場合のCSSはh1とarticleの間隔はheaderとsectionで空けるのでh1にはマージンは入れない。
h1{
margin-bottom: 0;
}
しかし、状況によってh1の直後にp要素のリードが付いたり付かなかったりする場合があるとする。
この場合、つまりh1の直後にp要素が来たらh1のマージンがゼロなのでp要素とくっついてしまう。そのためh1自身にマージンを追加して、直後のp要素との間隔を空ける必要がある。
<header>
<h1>とある王国の物語</h1>
<p>今、ハズクラス疑似王国は物価高が極まり<br>領民は日々の生活にあえいでいた。</p>
</header>
<section>
<article>領民のとるべき道は</article>
</section>
そこで:has()疑似クラスの登場、下記のように記述することで、h1直下にp要素がある場合にマージンを調整することができるようになる。
h1:has(+ p) {
margin-bottom: 20px;
}
:has()疑似クラスを使えば、『もしもh1の直下にp要素が来たらマージンを追加する。』というまるでIF文のような設定を入れられるんだ。
これが『:has()疑似クラスはCSSのIF文』と呼ばれるゆえんだね。
他にもいくつかサンプルがあるのでMDNを見ておくと良いと思う。
MDNの:has()疑似クラス解説ページ:https://developer.mozilla.org/ja/docs/Web/CSS/Reference/Selectors/:has
:has()疑似クラスができないこと
さっそく、国王を顔面蒼白にさせるデモンストレーション
:checked疑似クラスを監視し、その状態によって親のスタイルを制御する
顔面蒼白デモのコード解説
以下、使用コード。実装版はネスト化してコーディングしてあるので、見やすい様に元に戻して掲載した。
王に対する領民の行動をチェックボックスで選べるようにinput要素を3種類用意。
input要素にそれぞれallegiance、escape、revolutionのID名を付けてクリックできるように設置。
<!-- HTML -->
<div id="king" class="box">
<header class="castle">
<div class="title">国王</div>
<div id="king_avator" class="contents"></div>
</header>
<div class="kingdom">
<div class="title" id="knight">領民のとるべき道は</div>
<ul id="decision">
<li>
<input id="allegiance" type="radio" name="action">
<label for="allegiance">何が起きようと王に忠誠を誓おう:</label>
</li>
<!-- 以下同様に残り2つのラジオボタンにもチェックボタン設置 -->
</ul>
<div id="character"><img src="../images/hammerLady.webp" alt=""></div>
</div>
</div>
ポイントはここinput要素がchecked状態になったら王様の顔色を変更するように:has()疑似クラスで設定。
checkボタンはどれか一つだけを選択できるようにしたいのでラジオボタンを使用。
ボタンは、『忠誠、逃亡、革命』の3個設置。下記のように記述することでどのボタンがチェックされたか分かるようにした。
#king:has(#allegiance:checked){ /*王の顔色を肌色へ変更*/}
#king:has(#escape:checked){/* 王の顔色を赤色へ変更 */}
#king:has(#revolution:checked){/* 王の顔色を青へ変更 */}
/* 実際は下記のように他のスタイルも記載 */
#king:has(#allegiance:checked){
#king_avator{
background: #c9f0af;
svg #darma_face{
fill: #fde8eb;
}
}
}
さて、ここでもう一度重要な点をおさらい。分かりやすく中世ファンタジー風に解説するよ。
ポイントはこのコード#king:has(#revolution:checked){/* 王の顔色を変更 */}
これは:has()が設定されているのが#kingだから、領民が革命を選択すると王様が顔面蒼白になるんだ。
.kingdomだとこうはいかない。王様迄届かないんだよ、領民が何をやっても、王様の顔色は変わらない。#kingだからできるんだよ。
つまりこれは、領民が革命を行うことを王様自身が許容しているんだよね。
これってすごくない?
だって、普通の国王は自分が法律だよね。この国だってこれまでの国王は自分が法律だったはずだよ。だけど、疑似クラスHAS王は違う、賢王なんだよ。国を栄えさせるためにはどうすればいいか。
初めてすべての者が法のもとでは平等であることをhasクラスで宣言し、王でさえ、法の前では領民と同じく従うことを誓ったんだよ。
まさに賢王だね。
・・・と、中世ファンタジー風に:has()疑似クラスを紹介しました。
甚大な被害を受けたCSS達の一覧
has疑似クラスはやぱり、革命的で優秀なので影響を受けたCSSがいっぱいあるんだよね。早い話『必要なくねえ?』ってやつだね。
次にその一覧を表示にしたよ。
CSS本来の役割hasがあると
:focus-within 子孫にfocusがあるか検知 :has(:focus)で代用可能
:target-within 子孫にtargetがあるか検知 :has(:target)で代用可能
:checked連携 チェックボックスハック :has(input:checked)の方が自然
:valid連携 フォーム状態反映 form:has(:valid)で親まで伝播
:invalid連携 エラー表示 form:has(:invalid)で十分
:placeholder-shown連携 入力済み判定 :has(input:not(:placeholder-shown))
:empty 子要素有無判定 条件によってはhasで代替
details[open] 開閉状態判定 details:has(summary:focus)など柔軟化
:nth-child()の一部 子要素数で分岐 hasの組み合わせで代替可能
隣接セレクタ + 前後関係判定 checkboxなどの見た目をカスタマイズするハックは不要
一般兄弟 ~ 兄弟関係判定 hasの方が読みやすい場合あり
たとえば上の隣接セレクタの『+』の説明。これは下記の様なハックが昔はよく使われてた。
「input の後ろに label を置いて input:checked + label::before でスタイルを当てる」という考え。
inputの直後にlabel要素を置くのが必須。
<!-- HTML -->
<label class="custom-checkbox">
<input type="checkbox" />
<span>チェックする</span>
</label>
<!-- CSS -->
<style>
/* チェックボックス自体を非表示にし、隣接ラベルでスタイルを定義するCSS */
/* 以下二つのスコープがこのハックの要 */
.hidden-checkbox:checked + .checkbox-label::before {
background: #007bff;
}
.hidden-checkbox:checked + .checkbox-label::after { /* チェックマーク */
content: "";
position: absolute;
left: 7px;
top: 3px;
display: block;
width: 6px;
height: 11px;
border: solid white;
border-width: 0 3px 3px 0;
transform: rotate(45deg);
}
/* 以下はinput要素を隠したり、チェックアイコンを作成するCSS今回のテーマとは関係ない */
.hidden-checkbox { /* 画面外へ隠す */
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
}
.checkbox-label {
position: relative;
padding-left: 28px;
cursor: pointer;
}
.checkbox-label::before { /* ボックス枠 */
content: "";
position: absolute;
left: 0;
top: 0;
display: block;
width: 24px;
height: 24px;
border: 2px solid #ccc;
border-radius: 6px;
transition: all 0.2s ease;
}
.hidden-checkbox:checked + .checkbox-label::before {
background: #007bff;
}
.hidden-checkbox:checked + .checkbox-label::after { /* チェックマーク */
content: "";
position: absolute;
left: 7px;
top: 3px;
display: block;
width: 6px;
height: 11px;
border: solid white;
border-width: 0 3px 3px 0;
transform: rotate(45deg);
}
</style>
これが:has()疑似クラスの登場で、下記のように直感的に自由に記述することが可能になった。
コードの量も多少は減った。
<!-- HTML -->
<label class="custom-card">
<input type="checkbox">
<span>チェックする</span>
</label>
<!-- CSS -->
<style>
/* 疑似クラス:has() を使った状態変化 */
/* チェックされたinputを持つ親(.todo-item)の中の、カスタムボックスを変化させる */
.todo-item:has(.real-checkbox:checked) .custom-box {
background-color: #007bff;
border-color: #007bff;
transform: scale(1.05);
}
/* チェックマークを描画 */
.todo-item:has(.real-checkbox:checked) .custom-box::after {
transform: rotate(45deg) scale(1);
}
/* 以下はinput要素を隠したり、チェックアイコンを作成するCSS、今回のテーマ『:has()疑似クラス』とは関係ない */
/* 全体のレイアウト */
.todo-item {
display: flex;
align-items: center;
gap: 12px;
cursor: pointer;
padding: 10px;
}
/* 本物のチェックボックスは非表示に */
.real-checkbox {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
/* 1. カスタムチェックボックスの土台 */
.custom-box {
width: 24px;
height: 24px;
border: 2px solid #ccc;
border-radius: 6px;
position: relative;
transition: all 0.2s ease;
}
/* 2. チェックマークの線(擬似要素で作成) */
.custom-box::after {
content: "";
position: absolute;
left: 7px;
top: 3px;
width: 6px;
height: 11px;
border: solid white;
border-width: 0 3px 3px 0;
transform: rotate(45deg) scale(0); /* 初期状態は非表示 */
transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
</style>
:has()疑似クラス登場前のハックでは、 input が必ず先頭にないとCSSで制御できなかったが、:has() 登場後は input がテキストの後ろにあろうが、深い階層(子孫要素)にあろうが関係なくchecke状態を検知できるようになった。
label で input を囲むシンプルな構造にするだけで動作するため、管理が非常に楽。
このテーマ、『今までの面倒な記述を:has()疑似クラスを使えばこんなに楽になるよ、や、できるようになったよ』は、ほんとにたっくさんあるので、また折を見て紹介しようと思う。