CSS 疑似クラス :has() の使い方

作成日:2024年4月3日

:has() 擬似クラスとは

:has() はその要素が特定の子(孫)要素や兄弟要素を持っていたり、それらが特定の状態にある場合にスタイルを適用できる関数型擬似クラスです(リレーショナル擬似クラスとも呼ぶようです)。

引数のセレクターに結合子を指定して親子や兄弟関係を判定したり、擬似クラス(:hover など)の指定によりその状態を判定して、関係や状態に応じてスタイルを適用することができます。

これまでは JavaScript を使わなければできなかったような操作が CSS のみでできるようになります。

例えば以下の HTML で、p 要素の子孫に span 要素が存在すれば、p 要素に下線を表示するには

 <p>Lorem ipsum dolor sit, amet consectetur adipisicing elit.</p>
 <p>Cupiditate, saepe! <span>Reiciendis</span> voluptates nisi!</p>
 <p>Eum ullam consequatur porro neque a quam itaque magnam.</p>

以下のように記述できます。

p 要素が span 要素を持っていれば(p has span)、text-decoration: underline を適用します。

p:has(span) {
  text-decoration: underline;
}

この例の場合、2行目の p 要素が span 要素を持っているので下線が表示され、それ以外の p 要素には span 要素がないので下線は表示されません。

以下のサンプルではわかりやすいように、別途 CSS で span 要素に色(tomato)を設定しています。

Lorem ipsum dolor sit, amet consectetur adipisicing elit.

Cupiditate, saepe! Reiciendis voluptates nisi!

Eum ullam consequatur porro neque a quam itaque magnam.

:has() が使えない場合は、例えば、以下のような JavaScript を記述する必要がありました。

document.querySelectorAll('p').forEach((elem) => {
  // 以下の if 文が p:has(span) に該当
  if(elem.querySelector('span')) {
    elem.style.setProperty('text-decoration', 'underline');
  }
});

サポート状況

サポート状況の詳細は以下で確認できます。

https://caniuse.com/css-has

「Baseline 2023 Newly available across major browsers」となっていて、MDN には「2023年12月以降、この機能は最新のデバイスとブラウザーのバージョンで動作します。 この機能は、古いデバイスやブラウザでは動作しない可能性があります。」と記載されています。

古いブラウザへの対応

古いブラウザの対応が必要な場合は、以下のようなプラグインがあります。

PostCSS Has Pseudo

基本的な使い方

:has() の基本的な構文は以下のようになります。

対象のセレクターが引数に指定したセレクターを持っていれば、スタイルが適用されます。

対象のセレクター:has(相対セレクターリスト) {
  /* マッチする場合に適用するスタイル */
}

引数には >+~ などの結合子で始まる相対セレクターやそれらのカンマ区切りのリストを指定することができます。結合子を指定しない(省略した)場合は、子孫結合子が指定されたとみなされます。

また、様々なセレクターやそれらを組み合わせて指定することができ、例えば、擬似クラスセレクター(例 input:invalid や a:hover)などを使って状態を指定することもできます。

以下の場合、結合子なしで span と a:hover を指定しているので、子孫結合子が指定されたとみなされ、p 要素の子孫に span または a:hover があれば、text-decoration: underline が適用されます。

引数にカンマ区切りのリストを指定した場合、いずれかのセレクターが存在する(マッチする)場合にスタイルが適用されます。

p:has(span, a:hover) {
  text-decoration: underline;
}

/* span 要素がわかりやすいように色を指定。:has() の使用とは関係ありません */
p span {
  color: tomato;
}

以下の HTML の場合、

 <p>Lorem ipsum dolor sit, amet <a href="#">consectetur</a> adipisicing elit.</p>
 <p>Cupiditate, saepe! <span>Reiciendis</span> voluptates nisi!</p>
 <p>Eum ullam consequatur porro neque a quam itaque magnam.</p>

2番目の span 要素を持つ P 要素に下線が表示されます。また、a:hover を指定しているので、最初の P 要素の a 要素にホバーすると、p 要素に下線が表示されます。

Lorem ipsum dolor sit, amet consectetur adipisicing elit.

Cupiditate, saepe! Reiciendis voluptates nisi!

Eum ullam consequatur porro neque a quam itaque magnam.

上記の例の場合、以下のように直接の子要素であることを表す子結合子 > を指定しても結果は同じです。

p:has(> span, > a:hover) {
  text-decoration: underline;
}

また、上記は :is() を使って以下のように記述することもできます。

p:has(> :is(span, a:hover)) {
  text-decoration: underline;
}

:has() の連結

:has(A):has(B) のように :has() を連結することができます。

この場合、両方の条件を満たすセレクタにスタイルが適用されます。:has() を複数連結すると、それら全てのセレクターがマッチする場合にスタイルが適用されます。

以下は p 要素の子孫に span 要素があり、且つ href 属性が "#foo" の a 要素があり、リンクにホバーした場合に p 要素に下線を表示します。

p:has(span):has(a[href="#foo"]:hover) {
  text-decoration: underline;
}

以下の HTML の場合、

<p>Lorem <span>ipsum</span> dolor sit, amet <a href="#">consectetur</a> adipisicing elit.</p>
<p>Cupiditate, saepe! <span>Reiciendis</span> voluptates nisi!</p>
<p>Eum <span>ullam</span> consequatur <a href="#foo">porro</a> neque a quam itaque magnam.</p>

3つ目の p 要素のリンクにホバーすると下線を表示します。

Lorem ipsum dolor sit, amet consectetur adipisicing elit.

Cupiditate, saepe! Reiciendis voluptates nisi!

Eum ullam consequatur porro neque a quam itaque magnam.

1つ目の p 要素は、子孫に span 要素と a 要素を持ちますが、href 属性の値が "#foo" ではないのでスタイルは適用されません。

兄弟結合子との組み合わせ

次兄弟結合子と組み合わせると、直後に特定の弟要素を持つ兄要素にスタイルを適用することができます。

以下は次兄弟結合子 (+) を使って h3 要素の直後に続く p 要素に foo クラスが指定されている場合に、h3 要素にスタイルを適用する例です。

h3:has(+ p.foo) {
  color: forestgreen;
  border: 1px solid #999;
  display: inline-block;
  padding: .25rem 1rem;
}

以下の場合、1つ目の h3 要素にはスタイルが適用されますが、2つ目の h3 要素は直後に続く p 要素に foo クラスが指定されていないのでスタイルは適用されません。

<h3>Heading</h3>
<p class="foo">Lorem ipsum dolor sit amet consectetur, adipisicing elit. </p>
<h3>Heading</h3>
<p>Reiciendis, mollitia! Vero molestias veritatis, dolore cupiditate excepturi!</p>

Heading

Lorem ipsum dolor sit amet consectetur, adipisicing elit.

Heading

Reiciendis, mollitia! Vero molestias veritatis, dolore cupiditate excepturi!

制限事項

現時点では以下のような制限があります。

  • 入れ子にできない::has() 疑似クラスは入れ子にできません。:has() の中の :has() は無効です。
  • 疑似要素は無効::has() 内では ::before などの疑似要素は有効なセレクターではありません。

詳細度

:has() 疑似クラスは :is() と同様、引数の中で最も大きい詳細度が :has() の詳細度として与えられます。

例えば、以下の場合、span.foo はクラスセレクターを使っているので、単純に p 要素に指定するよりも詳細度が高く、後から指定した background-color: khaki では上書きされません。

p:has(span.foo) {
  background-color: antiquewhite;
}

p {
  background-color: khaki;
}

詳細度を上げたくない場合は :where() を使って、詳細度を 0 にすることができます。

以下は :where() で :has(span.foo) の詳細度を 0 にしているので、p 要素には後から指定した background-color: khaki が適用されます。

p:where(:has(span.foo)) {
  background-color: antiquewhite;
}

p {
  background-color: khaki;
}

:has() を使ったサンプル

代表的な(?):has() の使用例です。

カードレイアウト

:has()を使用して、カードレイアウトで画像がある場合とない場合で異なるスタイルを適用する例です。

画像がある場合は display:flex; を適用して画像とコンテンツを横並びにします。

Title 1

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aspernatur iusto laborum recusandae quos.

Title 2

Enim blanditiis eaque, quisquam incidunt autem maiores laborum inventore nobis veritatis.

Alias explicabo deleniti error eius dolores laudantium.

Title 3

Corporis numquam corrupti voluptatum nam mollitia libero quam accusamus officia obcaecati?

以下は上記カードレイアウトの HTML です。

<div class="card-wrapper">
  <div class="card">
    <div class="card-img">
      <img src="images/image-01.jpg" alt="">
    </div>
    <div class="card-content-wrapper">
      <h3 class="card-title">Title 1</h3>
      <div class="card-content">
        <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aspernatur iusto laborum recusandae quos.</p>
      </div>
    </div>
  </div>
  <div class="card">
    <div class="card-content-wrapper">
      <h3 class="card-title">Title 2</h3>
      <div class="card-content">
        <p>Enim blanditiis eaque, quisquam incidunt autem maiores laborum inventore nobis veritatis.</p>
        <p>Alias explicabo deleniti error eius dolores laudantium. </p>
      </div>
    </div>
  </div>
  <div class="card">
    <div class="card-img">
      <img src="images/image-02.jpg" alt="">
    </div>
    <div class="card-content-wrapper">
      <h3 class="card-title">Title 3</h3>
      <div class="card-content">
        <p>Corporis numquam corrupti voluptatum nam mollitia libero quam accusamus officia obcaecati?</p>
      </div>
    </div>
  </div>
</div>

.card の中に .card-img がある場合のみ、.card に display: flex を指定して画像とコンテンツのテキストを横並びにしています。

そして、.card の中に .card-img がある場合は、.card-content-wrapper の幅を調整し、左側にマージンを設定しています。

* {
  box-sizing: border-box;
}

.card-wrapper {
  display: flex;
  flex-wrap: wrap;
  max-width: 1200px;
  margin: 20px auto;
}

.card {
  border: 1px solid #ccc;
  border-radius: 8px;
  padding: 1rem;
  margin: 0 10px 20px 10px;
  width: calc((100% - 40px) / 2);
}

.card-img {
  aspect-ratio: 1/1;
  width: 40%;
  max-width: 300px;
  flex-shrink: 0;  /* 縮まずにオリジナルサイズを維持(または flex: 0 0 auto) */
}

.card-img img {
  width: 100%;
  height: 100%;
  object-fit: cover; /* コンテンツボックスに収まるように拡大縮小 */
}

.card-title {
  font-size: 18px;
  margin-top: 0;
}

.card-content {
  margin-top: 10px;
}

/* .card の中に .card-img がある場合のみ、.card に display: flex を指定 */
.card:has(.card-img) {
  display: flex;
  flex-wrap: wrap;
}

/* .card の中に .card-img がある場合の .card-content-wrapper のスタイル */
.card:has(.card-img) .card-content-wrapper {
  margin-left: 20px;
  width: calc(60% - 20px);
}

フォームの検証結果によりスタイルを設定

入力値の検証結果により、無効な場合はエラーメッセージを表示し、ラベルの文字色を変更する例です。

フォームの入力値が有効でない(検証に失敗した)状態は、:invalid 擬似クラスで、入力値が有効な場合は :valid 擬似クラスで判定できます。

また、プレースホルダが表示されている状態(未入力の状態)は :placeholder-shown、入力中の状態 は :focus 擬似クラスで判定できます。

以下は入力されたメールアドレスが正しい形式でない場合はエラーメッセージを表示し、ラベルの色を状態により変更します。

<form class="my-form">
  <div class="form-control">
    <label for="email">Email: </label>
    <input type="email" id="email" required pattern="([a-zA-Z0-9\+_\-]+)(\.[a-zA-Z0-9\+_\-]+)*@([a-zA-Z0-9\-]+\.)+[a-zA-Z]{2,6}" placeholder="メールアドレス">
  </div>
  <div class="form-control">
    <label for="tel">TEL: </label>
    <input type="tel" id="tel" pattern="0{1}\d{1,4}-{0,1}\d{1,4}-{0,1}\d{4}" placeholder="電話番号(半角数字)">
  </div>
</form>

以下では、form.my-form:has(input:invalid:not(:placeholder-shown)) で form.my-form に無効な入力値(input:invalid)がある場合は、疑似要素(::after)のエラーメッセージを表示しています。

その際に、:not(:placeholder-shown) を指定してプレースホルダが表示されている状態ではエラーを表示しないようにしています。また、input:focus:invalid でフォーカスしている間は入力値が無効でもエラーを表示しないようにしています。

ラベルのスタイリングでは、次兄弟結合子 (+) を使って label 要素の直後に続く input 要素が有効かどうかで文字色を切り替えています。

form.my-form {
  position: relative;
}

form.my-form:has(input:invalid:not(:placeholder-shown))::after {
  /* 入力値が有効でなければエラーメッセージ表示 */
  display: block;
}

form.my-form:has(input:focus:invalid:not(:placeholder-shown))::after {
  /* フォーカス中はエラーメッセージを表示しない */
  display: none;
}

label:has(+ input:invalid:not(:placeholder-shown)) {
  /* 入力値が有効でなければラベルを赤色に */
  color: tomato;
}

label:has(+ input:valid:not(:placeholder-shown)) {
  /* 入力値が有効であればラベルを緑色に */
  color: green;
}

/* エラーメッセージを擬似要素で作成 */
form.my-form::after {
  display: none;
  content: "入力値が正しくありません。";
  color: tomato;
  position: absolute;
  left: 0;
  bottom: -2rem;
  font-size: 13px;
}

input {
  border: 1px solid #ccc;
  padding: 5px;
  width: 240px;
}

.form-control {
  margin: 20px 0;
}

label {
  display: inline-block;
  width: 40px;
  font-size: 14px;
  color: #777;
}

:user-invalid を使う

:invalid:valid 擬似クラスはページをロードした直後の初期状態や入力中に評価されてしまうので、上記では :not(:placeholder-shown) や input:focus を使って対処しています。また、:not(:placeholder-shown) を使用しているため、上記の例の場合、required に対する検証が行われません。

:user-valid や :user-invalid はユーザーの操作が行われたあとに検証を行います。

以下は :invalid 擬似クラスの代わりに、:user-invalid 擬似クラスを使用する例です。また、以下では CSS ネスティングの入れ子構文を使っています。

form.my-form {
  position: relative;

  /* :user-invalid を使用 */
  &:has(input:user-invalid)::after {
    display: block;
  }

  /* :user-invalid を使用 */
  label:has(+ input:user-invalid) {
    color: tomato;
  }

  /* 有効な場合は、入力中に評価されても良いので :valid を使用 */
  label:has(+ input:valid:not(:placeholder-shown)) {
    color: green;
  }

  &::after {
    display: none;
    /* エラーメッセージを変更 */
    content: "入力値が正しくないか、入力漏れがあります。";
    color: tomato;
    position: absolute;
    left: 0;
    bottom: -2rem;
    font-size: 13px;
  }

  input {
    border: 1px solid #ccc;
    padding: 5px;
    width: 240px;
  }

  .form-control {
    margin: 20px 0;
  }

  label {
    display: inline-block;
    width: 40px;
    font-size: 14px;
    color: #777;
  }
}

HTML は input#email のプレースホルダー(以下では「※必須」を追加)以外は同じなので省略します。

:user-invalid のサポート状況

:user-invalid は最新のモダンブラウザではサポートされていますが、古いブラウザではサポートされてないものもあります。詳細は以下で確認できます。

https://caniuse.com/mdn-css_selectors_user-invalid

関連ページ

ラジオボタンで背景色を切り替え

子要素のラジオボタンの状態で、別の子要素の背景色と文字色を変更する例です。

Lorem ipsum dolor sit amet consectetur adipisicing elit. Vel ab ipsum repudiandae voluptate deserunt voluptatibus sequi magnam aliquam perspiciatis laudantium temporibus ducimus maxime sit illo quibusdam quo, minus accusamus adipisci.

Natus eum, similique cumque tempora tenetur ea quia velit a aliquam minus numquam saepe eius, dolor, dolorem veniam! Deserunt quidem recusandae commodi totam rem, minima sunt sequi voluptatum? Incidunt, veritatis.

<div class="bgc-content-wrapper">
  <div class="bgc-content">
    <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. ...</p>
    <p>Natus eum, similique cumque tempora tenetur ea quia velit ...</p>
  </div>
  <div class="controls">
    <input type="radio" name="mode" id="light" checked>
    <label for="light"> Light </label>
    <input type="radio" name="mode" id="dark">
    <label for="dark"> Dark </label>
  </div>
</div>

.bgc-content-wrapper の子要素の2つのラジオボタンの選択状態(input#light:checked と input#dark:checked)により、.bgc-content-wrapper の子要素の .bgc-content の背景色や文字色を切り替えています。

.bgc-content-wrapper {
  max-width: 960px;
  margin: 20px auto;
}

.bgc-content-wrapper .bgc-content {
  padding: 1rem;
  border: 1px solid #927044;
}

.bgc-content-wrapper:has(input#light:checked) .bgc-content {
  background-color: #fbf5ec;
  color: #643901;
  transition: color .2s, background-color .2s;
}

.bgc-content-wrapper:has(input#dark:checked) .bgc-content {
  background-color: #252f14;
  color: #d7d5d5;
  transition: color .2s, background-color .2s;
}

.bgc-content-wrapper .controls {
  margin-top: 20px;
}

.bgc-content-wrapper input:checked+label {
  color: tomato;
}