今回は新しい擬似クラス(セレクター関数)の:has()の使い方について解説します。
これまでjavascriptを利用していた「子の状態を見て親の見た目を変える」といったことがCSSの:has()を利用することで、CSSのみで実現できるようになります。
「子ども(や後続)を条件に、親(や前)のスタイルを適用する」のが最大の魅力で、フォーム検証など実務で即効性があります。
この記事で :has() の基本(使い方・考え方) をつかんで、すぐに使いこなせるようにしましょう。
目次
セレクターとは
まず、セレクターとは「どの要素にCSS(スタイル)を当てるか」を指名する書き方のことです。
簡単な例でいうと、下記は p
タグ全ての要素に line-height: 1.8
のCSSを当てています。
また、a:hover { opacity: .8; }
で a
タグの要素にマウスが乗ったら opacity: .8
のCSSを当てています。
p { line-height: 1.8; } /* p 要素ぜんぶ */
a:hover { opacity: .8; }
今回の :has()
もその要素に、CSS(スタイル)を当てるかどうかを判定する新しいセレクターです。
CSS の :has() とは
“親・前”を適用するかの条件
A:has(B)
は、「A の内側(子孫)や兄弟関係に B が存在するなら A を適用する」つまり、AがBを持っていたら・Aの兄弟にBがいたら中身のスタイルを適用するという意味です。
たとえば「section
の中に video
があれば、その
にだけ余白調整のスタイルを適用する」は次のように書けます。section
section:has(video) {
padding: 10px;
}
以下のようにsection
に<video>
がある<section>
のみにこの padding: 10px
が適用されます。
<!-- 適用(videoが内側にある) -->
<section class="intro-video">
<h2 class="intro__title">概要</h2>
<p>セクション説明</p>
<video>____</video>
</section>
<!-- ✖️ 適用されない(videoが内側にない) -->
<section class="intro">
<h2 class="intro__title">概要</h2>
<p>セクション説明</p>
</section>
<!-- ✖️ 適用されない(videoが外側にある) -->
<section class="intro">
<h2 class="intro__title">概要</h2>
<p>セクション説明</p>
</section>
<video>____</video>
この「内側を見て(条件を判定して)、親を選ぶ(適用する)」が :has() の本質です。
条件の中にはORやANDを利用できます。
カンマで OR、連結で ANDになります。
body:has(article, aside) {
padding: 24px;
}
上記のように , カンマでタグを区切れば、 bodyの中に articleタグもしくは、asideタグがある場合は、bodyにpadding: 24px
を適用しています。
また、下記のようにhas() を連結させれば、bodyの中にarticleタグかつ、asideタグがある場合は、bodyにpadding: 24px
を適用しています。
body:has(article):has(aside)
{
padding: 24px;
}
結合子(コンビネータ)を使って適用させる
:has()の中に結合子を利用することで負荷が軽くなります。
先程のbody:has()
や、*:has()
は条件が広く、計算コストが高くなりますので、なるべく結合子を使って探索範囲を絞り、負荷を軽くしましょう。
例えば、先程の「section
の中に video
があれば、その
にだけ余白調整する」ですが、「section
section
の直後にvideo
があるときのみ」だった場合は、以下のように、 + の結合子を付けることで直後という条件になります。
section:has(+ video) {
padding: 10px;
}
以下のように<section>と同列(兄弟)で<section></section>の直後(+)に<video>があった場合は適用となります。
<!-- ✖️ 適用されない(<video>はsectionの兄弟でない) -->
<section class="intro">
<h2 class="intro__title">概要</h2>
<p>セクション説明</p>
<video>____</video>
</section>
<!-- 適用(<video>はsectionと同列兄弟で直後にある) -->
<section class="intro-video">
</section>
<video>____</video>
<!-- ✖️ 適用されない(<video>はsectionの兄弟だけど直後でない) -->
<section class="intro-video">
</section>
<img>
<video>____</video>
+
は直後兄弟の結合子です。:has()
の引数は相対セレクタなので、>
, +
, ~
, スペース を使えます。
>
, +
, ~
, スペース について、使い方はこの後で詳しく解説しています。
has()の注意点と対応ブラウザ
has()の特異性
特異性(specificity)とは、同じ要素に複数のルールが当たったとき「どのルールを優先するか」を決める重みのことです。
特異性のルール(下に行くほど弱い)
!important
インラインスタイル:style="..."
ID セレクタ:#id
クラス / 属性 / 擬似クラス:.class
、[attr]
、:hover
、:invalid
など
要素 / 擬似要素:h1
、::before
※ 同じ強さなら最後に書いたほうが優先されます。
has()は () の中で最も強いものを引き継ぎます。
<div class="card">
<span class="badge"></span>
<span id="vip"></span>
<p class="title">ここは何色になるか?</p>
</div>
このようなHTMLがあった場合で、下記のCSSで、has()を定義するとtitleクラスの色は何になると思いますか?
.title { color: black; }
p { color: red; }
.card:has(#vip) .title { color: blue; }
.card:has(.badge) .title { color: green; }
サンプルの答え
ここは何色になるか?
青色になりました。最も強い#vip(IDセレクタ)のスタイルが引き継がれています。
has()の制約
:has()
を入れ子にできません。A:has(B:has(C))
のような:has()
の中の:has()
は無効- パフォーマンス上の注意(特にDOMが頻繁に変わるUI)
ブラウザの最適化は進んではいますが、できるだけ引数で>
や+
を使い、ツリー全探索を減らしましょう。
has()の対応ブラウザ
2025年現在 Can I use ? から以下のようになっています。
ブラウザ | 対応バージョン |
---|---|
Chromium系(Chrome/Edge/Opera/Samsung Internet) | Chrome 105+ / Edge 105+ / Opera 91+ / Samsung Internet 20+ で対応。 Android版も同様 |
Safari(macOS / iOS) | 15.4+ で対応(iOS 15.4+ も同様) |
Firefox(デスクトップ / Android) | 121+ で正式対応 |
主要ブラウザは概ね完全対応済みなので、問題なく利用できます。
iOS15は2025年現在、Apple公式でまだサポートされていますが、現状、ほとんどのサービスでサポート終了している状況です。
ただし、古いブラウザ(特にiOS)も完全にカバーする必要がある場合は利用しないほうが安全です。
結合子を使った条件式
ここからは結合子などを使った条件式の書き方についていくつか解説します。
A:has(B C)
スペース
で区切ると、Aの中のBの中にCがあれば適用する。となります。
.form-row:has(.field .error) {
gap: .25rem;
}
<div class="form-row">
<div class="field">
<small class="error">エラー</small>
</div>
</div>
A:has(> B)
>
はAの子にBがあれば適用します。
.form-row:has(> .field) {
gap: .25rem;
}
<div class="form-row">
<div class="field">
<small class="error">エラー</small>
</div>
</div>
A:has(B)
と A:has(> B)
の違い
>
がない場合は内側にBがあれば適用されます。(どこでもよい)
>
がつくと子に限定されます。
/* 1. > なし */
.form-row:has(.field) {
gap: .25rem;
}
/* 2. > あり */
.form-row:has(> .field) {
gap: .35rem;
}
以下のHTMLの場合は、
1. の > がない方は適用されます。内側にあるからです。(どこにあってもいい)
2.の > がある方は適用されません。子ではないからです。
<div class="form-row"> <!-- 1. gap: .25rem; が適用される -->
<div class="contents">
<div class="field"> <!-- ここに.filedがある -->
<small class="error">エラー</small>
</div>
</div>
</div>
A:has(~ B)
Aの後ろ側のどこかの兄弟にBがあれば適用します。
.form-row:has(~ .field) {
gap: .25rem;
}
<!-- 適用される -->
<div class="form-row">
</div>
<div class="field">
<small class="error">エラー</small>
</div>
以下は.field
クラスがform-row
クラスの前にいる、後側にないため適用されません。
<!-- これは適用されない(.fieldがorm-rowの前にいる、後側にない) -->
<div class="field">
<small class="error">エラー</small>
</div>
<div class="form-row">
</div>
A:has(~ B) と A:has(+ B)の違い
A:has(+ B)
は直後の兄弟になります。
A:has(~ B)
は後ろ側の兄弟であればどこにいてもいいです。
/* 1. + */
.form-row:has(+ .field) {
gap: .25rem;
}
/* 2. ~ */
.form-row:has(~ .field) {
gap: .35rem;
}
以下のHTMLの場合は、
1. の + は適用されません。field
のクラスはform-row
クラスの兄弟ですが、直後にないからです。
2.の ~ は適用されます。兄弟でform-row
の後側ならどこにあってもいいからです。
<div class="form-row">
</div>
<video>.....</video>
<div class="field"> <!-- ~ は適用される -->
<small class="error">エラー</small>
</div>
A:has(B > C)
や A:has(> B C)
このように複数条件を書くことができます。さらにOR( , )や、AND( :has():has() )も入れることができます。
form-row内にある .fieldクラスの子の.errorクラスがあれば 色をredに変える。
.form-row:has(.field > .error ) {
color: red;
}
form-row内の子の .field-passクラス内に.warnクラスがあれば 色をyellowに変える。
.form-row:has(> .field-pass .warn ) {
color: yellow;
}
form-row内の子の .fieldクラスの中に .success もしくは .infoがあれば色をblueに変える。
.form-row:has(> .field .success , > .field .info ) {
color: blue;
}
結合子の補足
なお、結合子はhas()の中だけでなく、CSSの適用条件としてもよく使われます。
いくつかサンプルを出しておきますので、この機会に習得しましょう。
第1階層のメニューだけスタイルを適用
.menu の「子」li だけ適用し、.submenu(サブメニュー)は当てない。
.menu > li { padding: .5rem 1rem; }
.menu > li > a { font-weight: 600; }
/*
このように書くと全部に(サブメニューにも)当たってしまう
.menu li { padding: .5rem 1rem; }
.menu li a { font-weight: 600; }
*/
<ul class="menu">
<li><a href="#">Home</a></li>
<li>
<a href="#">Products</a>
<ul class="submenu">
<li><a href="#">A</a></li>
<li><a href="#">B</a></li>
</ul>
</li>
<li><a href="#">Contact</a></li>
</ul>
連続段落だけ余白を詰める
直後も p のとき(=連続段落)だけ上マージンをゼロにする
p + p { margin-top: 0; }
<p>本文1。段落。</p>
<p>本文2。直後に続く段落。</p>
<p>本文3。さらに続く段落。</p>
リストの“2個目以降”だけボーダー
.list の子の li のうち「直後も li」のものだけ(=2個目以降)に適用する
.list > li + li {
border-top: 1px solid #e5e7eb;
padding-top: .5rem;
}
<ul class="list">
<li>項目1</li>
<li>項目2</li>
<li>項目3</li>
</ul>
実践サンプル
基本的な:has()の書き方を理解したところで、ここからは実践で使える簡単なサンプルをいくつか解説します。
全てJavascript不要です。結合子や擬似クラスなどを使ったいろいろな書き方があります。
リンクに「新規で開く」が含まれるときだけアイコン表示
<p class="post">参考:<a href="#" target="_blank" rel="noopener">外部サイト</a></p>
<p class="post">社内リンク:<a href="#">ポリシー</a></p>
.postクラスの内部で、 aタグに target=_blank (新規タブで開く)があれば、記事の末尾に疑似要素(::after)のアイコンをつけています。
.post:has(a[target="_blank"])::after {
content: "↗";
margin-left: .2rem;
font-size: .85em;
color: #64748b;
}
a[target=”_blank”]の […] は属性セレクタです。
要素の 属性の有無や値 で絞り込むときに使います。
属性セレクタ | 意味 | サンプル |
---|---|---|
[attr=”val”] | 属性の値がvalの場合 | a[target=”_blank”] input[type=”hidden”] |
[attr^=”val”] | 属性の値がvalから始まる | a[rel^=”noop”] |
[attr*=”val”] | 属性の値にvalが含まれる | a[rel*=”op”] |
[attr$=”val”] | 属性の値がvalで終わる | a[rel$=”noop”] |
[hidden] | hidden属性がある | <div hidden></div> [hidden] { display: none; } |
他にもありますが少し本筋から外れてしまうので、ここまでにします。
セクション見出しがアンカー対象(:target)になったらセクションを強調
<nav class="toc">
<a href="#faq-1">Q1</a>
<a href="#faq-2">Q2</a>
</nav>
<section class="faq">
<h2 id="faq-1">Q1</h2>
<p>回答…</p>
</section>
<section class="faq">
<h2 id="faq-2">Q2</h2>
<p>回答…</p>
</section>
セクション直下の見出しが :target のとき、親 section を装飾させています。
.faq:has(> h2:target) {
outline: 2px solid #0ea5e9;
scroll-margin-top: 6rem; /* 固定ヘッダー対策にも */
}
:targetは擬似クラスです。他にも以下のようなものがあります。
a:hover { opacity:.8; } /* マウスが乗った時 */
input:focus { outline:2px solid #60a5fa; } /* その要素にフォーカス */
input:focus-within { outline:2px solid #60a5fa; } /* その要素自身 または その要素の内側のどれかにフォーカス */
input:invalid { border-color:#ef4444; } /* 検証NG(required/型/範囲など) */
input:valid { border-color:#22c55e; } /* 検証OK */
input:required { background:#fff; } /* 必須フィールド */
input:read-only { background:#f5f5f5; } /* 読み取り専用 */
input:disabled { opacity:.5; cursor:not-allowed; } /* 無効化 */
input:checked + label { font-weight:700; } /* チェック済み(チェックボックス/ラジオ) */
input:placeholder-shown { color:#9ca3af; } /* プレースホルダーが表示されている */
入力フォームでhasを使ったサンプル
以下のことをしています。
- 入力欄が必須(required) ならラベルに * を自動表示する
- 入力欄が必須(required)がないなら [スペース(*を色なし)] を自動表示する
- 入力が空でないときだけ「クリア」ボタンを表示
- 未入力/不正があればフォームを強調(赤で囲む)(フォーカス中は除外)
- 入力が空でない & 不正 & フォーカス外 のときだけエラーを表示
<form class="contact">
<label class="field-label" for="email">メール</label>
<input id="email" type="email" placeholder="aaaa@sample.com" required >
<small class="error" aria-live="polite">正しいメールアドレスを入力してください。</small>
<button type="button" class="clear" aria-label="clear">×</button>
<label class="field-label" for="name">お名前</label>
<input id="name" type="text">
</form>
/* 01. 入力欄が必須(required) ならラベルに * を自動表示する */
.field-label:has(+ input:required)::after {
content: "*";
color: #e11d48;
font-weight: 700;
}
/* 02. 入力欄が必須(required) でないなら スペース(*を色なし) を自動表示する */
.field-label:has(+ :not(input:required))::after {
content: "*";
color: transparent;
}
/* 03. 入力が空でないときだけ「クリア」ボタンを表示 */
.clear { display: none; }
.contact:has(#email[placeholder]:not(:placeholder-shown)) .clear {
display: inline-block;
}
/* 04 行の内側に :invalid がある かつ フォーカスしていない時だけ色を変える */
.contact {
padding: .5rem;
border: 1px solid transparent;
}
.contact:has(:invalid):not(:focus-within) {
background: #fff6f6;
border-color: #fca5a5;
}
/* 05 入力が空でない & 不正 & フォーカス外 のときだけエラーを表示 */
.error { display: none; }
.contact:has(#email:not(:placeholder-shown):invalid):not(:focus-within) .error {
display: inline;
color: #e11d48;
}
サンプル
メールのテキスト欄に[aaa]などメールアドレスではない値を入れてフォーカスを外してください。正しいメールアドレスの場合はエラーのスタイルやテキストが消えます。
最後に
いかがでしょうか?
:has()
を使うと、これまで JS で「親にクラスを足す」ために書いていた処理の多くが CSSだけで書けます。
まずは小さな箇所(直後兄弟 +
/子 >
を使うなど)から1つずつ試してみてください。
慣れてきたら、OR/ANDや:invalid
、:focus
との組み合わせに広げていき、無理なく、読みやすく保守しやすいCSSにしていきましょう。