「ある要素を含む場合」のスタイル指定がcssだけでできる!:has() を使う方法[css, :has()]

「ある要素を含む場合」のスタイル指定がcssだけでできる!:has() を使う方法[css, :has()]
今回はタイトルの通り、cssだけで
「ある要素を含む場合」を特定して、その要素を含む親要素にスタイル指定をする方法になります。

具体的には、
:has() という擬似クラスを使用します。

こちらの :has() 、昨年2022年の中頃までは対応ブラウザが少なく
便利なことはわかっているものの、実務での投入は二の足を踏む存在だったのですが

2023年3月現在、FireFoxを除く主要ブラウザのほぼ全てで対応されており、
実務での使用がかなり現実的なものとなってきました。

当記事の後半では、FireFox等、未対応ブラウザの場合の対処法についても記載していますので、
あわせてご覧ください。

それでは早速、:has() を使ったスタイル指定の方法について、次のセクションより書いていきたいと思います。


htmlとcssの基本構造

次の様な、1200pxの親要素内に、6枚のカードが並ぶ
カード型レイアウトがあったとします。

カード型レイアウトのサンプル

まずは、上記のレイアウトのhtml構造から書いていきます。

<div class="my-wrapper">
  <div class="my-container">
    <div class="my-card-container">
      <div class="my-card-thumbnail-container">
        <img class="my-card-thumbnail-item" src="https://picsum.photos/640/480?grayscale&random=1" alt="サムネイルのalt">
      </div>
      <div class="my-card-body-container">
        <h3>カードタイトル</h3>
        <p>カードテキスト</p>
        <p class="my-card-tag" data-tag="pizza">タグ:グルメ(ピッツァ・チーズ)</p>
      </div>
    </div>
    
    <div class="my-card-container">
      <div class="my-card-thumbnail-container">
        <img class="my-card-thumbnail-item" src="https://picsum.photos/640/480?grayscale&random=2" alt="サムネイルのalt">
      </div>
      <div class="my-card-body-container">
        <h3>カードタイトル</h3>
        <p>カードテキスト</p>
        <p class="my-card-tag" data-tag="osushi">タグ:グルメ(おすし)</p>
      </div>
    </div>
    
    <div class="my-card-container">
      <div class="my-card-thumbnail-container">
        <img class="my-card-thumbnail-item" src="https://picsum.photos/640/480?grayscale&random=3" alt="サムネイルのalt">
      </div>
      <div class="my-card-body-container">
        <h3>カードタイトル</h3>
        <p>カードテキスト</p>
        <p class="my-card-tag" data-tag="fried-chicken">タグ:グルメ(からあげ)</p>
      </div>
    </div>
  </div>
  <div class="my-container">
    <div class="my-card-container">
      <div class="my-card-thumbnail-container">
        <img class="my-card-thumbnail-item" src="https://picsum.photos/640/480?grayscale&random=4" alt="サムネイルのalt">
      </div>
      <div class="my-card-body-container">
        <h3>カードタイトル</h3>
        <p>カードテキスト</p>
        <p class="my-card-tag" data-tag="trip">タグ:旅の記録</p>
      </div>
    </div>
    
    <div class="my-card-container">
      <div class="my-card-thumbnail-container">
        <img class="my-card-thumbnail-item" src="https://picsum.photos/640/480?grayscale&random=5" alt="サムネイルのalt">
      </div>
      <div class="my-card-body-container">
        <h3>カードタイトル</h3>
        <p>カードテキスト</p>
        <p class="my-card-tag" data-tag="sea-food">タグ:グルメ(海鮮)</p>
      </div>
    </div>
    
    <div class="my-card-container">
      <div class="my-card-thumbnail-container">
        <img class="my-card-thumbnail-item" src="https://picsum.photos/640/480?grayscale&random=6" alt="サムネイルのalt">
      </div>
      <div class="my-card-body-container">
        <h3>カードタイトル</h3>
        <p>カードテキスト</p>
        <p class="my-card-tag" data-tag="osushi">タグ:グルメ(おすし)</p>
      </div>
    </div>
  </div>
</div>

続いて、上記htmlに適用するcssを記載します。

.my-container {
  display: flex;
  gap: 1rem;
  justify-content: center;
  margin: 3.5rem auto 0;
  width: 1200px;
}
.my-card-container {
  box-shadow: 0 1px 5px rgba(0, 0, 0, .5);
  border-radius: 8px;
  overflow: hidden;
  position: relative;
  width: 33.33333%;
}
.my-card-thumbnail-container {
  height: 180px;
  overflow: hidden;
  width: 100%;
}
.my-card-thumbnail-item {
  display: block;
  width: 100%;
}
.my-card-body-container {
  border-top: 3px solid rgba(205, 205, 205, 1);
  padding: 1.5rem;
}
.my-card-tag {
  background: rgba(150, 150, 150, 1);
  border-radius: 8px;
  color: #fff;
  display: inline-block;
  font-size: .625rem;
  padding: 0 .5rem;
}

↑上のhtmlとcssで、先ほどのキャプチャの様なカード型レイアウトの作成ができます。

ここに、次のキャプチャの様に、
特定のdata属性を持つカードだけRECOMMEND と記載された
おすすめ表示をつけてみる
ことにします。

:has()擬似クラスを使用した場合のカード型レイアウトサンプル

先ほどのcssに、次の記述を追加します。

.my-card-body-container:has( > p[data-tag="osushi"]) {
  border-top: 3px solid rgba(0, 200, 150, 1);
  font-weight: 800;
}
.my-card-body-container:has( > p[data-tag="osushi"])::before {
  background: rgba(0, 200, 150, 1);
  content: 'RECOMMEND';
  color: #fff;
  display: inline-block;
  font-size: .75rem;
  font-weight: 600;
  left: 0;
  padding: 0 .5rem;
  position: absolute;
  top: 1rem;
}

↑出ました。 :has() 擬似クラスです!

この :has() 擬似クラスを使うと、上記のキャプチャの様に
特定の条件を持つ子要素をふくんだ親要素だけ」をcssのみで特定して、
styleを変える事が可能になるというわけです。

上記の例では、特定のdata属性を各カードに持たせているサンプルのため、
.my-card-body-container:has( > p[data-tag=”osushi”])
として、data属性による要素の絞り込みを行っています。

これは別にdata属性でなくとも、
特定のidを持つ要素や
特定のclassを持つ要素など、
cssで対象を一意に絞り込める要素であれば
自由に指定が可能です。

:has() の登場以前は、この様な要素の絞り込みは
例えばJavaScriptを駆使する等のアプローチをするしか方法がなかったのですが、
それがcssのみで実現できるということで、
これはかなり使い道がありそうな手法かと思います。

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

こちらの擬似クラス :has() ですが、文字通り
要素名:has(条件)
↑この記法で指定した
条件に一致する親要素を選択する事ができます。

そのため、上記の例では、
6枚のカードがそれぞれ持っているclassである
.my-card-body-container の中の、

.my-card-body-container:has( > p[data-tag=”osushi”])
↑この条件に当てはまる要素だけに作用して
styleを当てる事ができている、というわけです。

このくらいの規模のサンプルだと、「 nth-of-type で指定すればいいんじゃないの?
と思われるかもしれませんが、「要素の並び順が固定」の「静的サイト」の場合は確かにそれでも大丈夫かもしれません。

この :has() が本当に真価を発揮するのは、主に動的サイトにおいてなのかなと思います。

たとえば動的に要素の順番が変わるサイトや、

同じクラス名を持った複数の親要素が並ぶ様なページ(例えば3カラムで何らかのリストが並んでいる様なページ)で、
なおかつユーザー操作によってその要素内の子要素の並び順、あるいは表示/非表示が変わる場合など。

そんな場合にも、一意の識別子(特定のid、特定のclass、あるいは特定の属性)で
ターゲットとなる子要素を特定、それを含む親要素に影響を与える事ができる
点が、
非常に強みを発揮する部分かと思います。

対応ブラウザはどうなのか。未対応ブラウザへのアプローチ方法など

こちらの :has() 、昨年2022年の中頃までは対応ブラウザが少なく、
便利ではあるものの、実務での投入は二の足を踏む存在だった実情があります。

時がたった2023年3月現在、FireFoxを除く主要ブラウザのほぼ全てで対応されるようになっており、
実務での使用がかなり現実的なものとなっています。

それでも、「FireFoxがダメなら、うちの案件では使えないな…」と思う方も多いかもしれませんが、
そこはJavaScriptを使って、次の様なアプローチをする事で補完する事が可能です。

そのブラウザで :has() 擬似クラスが使えるかどうかを判定するJavaScriptを使う

次の様なJavaScriptを書く事で、FireFoxのように :has() に対応していないブラウザに
対する補完をする事ができます。(他にもアプローチ方法はあるかと思いますが、一例です)

<script>
const isFireFox = !CSS.supports("selector(:has(*))");
if(isFireFox) {
  const hasTargetData = document.querySelectorAll('p[data-tag="osushi"]');
  hasTargetData.forEach((e) => {
    e.parentNode.classList.add('is-recommend');
  });
}
</script>

2行目では、CSS.supports() という WebAPI を使用して、そのブラウザで :has() 擬似クラスが使えるかどうかを判定しています。
WebAPI、と聞くと難しく聞こえるかもしれませんが、単純にJavaScriptから定型の構文で呼んであげるだけで使用できます。)

CSS.supports() Web API は、条件に一致すれば true、しなければ false を返してくれるので、
2行目の isFireFox には、その判定結果が格納されることになります。
それを利用しているのが3行目〜8行目までの部分。
:has() 擬似クラスを使用した場合と同じ結果を反映できる様に、
対象の親要素に別途 is-recommend というclassを付与してあげるということをしています。

e.classList.add(‘is-recommend’);
としてしまうと、4行目で指定しているp要素そのものに対しての処理となってしまうため
6行目のように
parentNode をつけて、親要素を対象にclassを付与している、という形になります。
(parentNode というのは、文字通り親要素を取得する記述になります。詳細はMDN内の次のページを参照)
Node.parentNode – Web API | MDN

:has() 擬似クラスを使用した場合と同じ結果となる様に、あらかじめ
css側には、次の記述を加えておきます。

.my-card-body-container.is-recommend {
  border-top: 3px solid rgba(0, 200, 150, 1);
  font-weight: 800;
}
.my-card-body-container.is-recommend::before {
  background: rgba(0, 200, 150, 1);
  content: 'RECOMMEND';
  color: #fff;
  display: inline-block;
  font-size: .75rem;
  font-weight: 600;
  left: 0;
  padding: 0 .5rem;
  position: absolute;
  top: 1rem;
}

↑これを記述しておく事で、
:has() 擬似クラスがサポートされていない場合には
先ほどのJavaScriptにより is-recommend というclassが
RECOMMEND表示をつけたいカードの親要素に付与されるため、
:has() 擬似クラスを使った場合と同じ結果を与える事ができる、というわけです。

[@supports記法]を使ったアプローチ

ここまでのセクションでは、:has() 擬似クラスを使って、特定の要素を含む親要素に対して変化を与える方法と、
それに対応していないブラウザを対象に、その補完を行う方法について書いてきました。

この他のアプローチ方法としては、cssの @supports記法による方法が考えられます。
具体的には、次のとおり。

@supports (selector(:has(*))) {
/* ...(ここに対応していた場合のcssを書く)... */
}

@supports not (selector(:has(*))) {
/* ...(ここに対応していなかった場合のcssを書く)... */
}

↑このように、
@supports (条件)
@supports not (条件)
とする事で、そのプロパティが対応していた場合の記述と、
そのプロパティが対応していない場合の記述を
分けて管理する事ができます。

もちろんこれを書いたとて、未対応ブラウザに効かせようとするならば、
JavaScript等によるアプローチは必要(あるいは何らかのPolyfillが必要)にはなりますが、
合わせ技としてこの @supports も知っておく事で、より柔軟なアプローチが取れることと思います。

MDN内の解説など

この記事で登場した、用語などに関するMDN内の詳しい解説は次のとおりとなります。
[:has() 擬似クラス]
:has() – CSS: カスケーディングスタイルシート | MDN

[CSS.supports() WebAPI]
CSS.supports() – Web API | MDN

[Node.parentNode]
Node.parentNode – Web API | MDN

[@supports記法]
@supports – CSS: カスケーディングスタイルシート | MDN

↑いずれも、深い知見が得られますので一読することをおすすめします。

この記事のまとめ

今回は、:has() という擬似クラスを使って
cssだけで「ある要素を含む場合」を特定して、その要素を含む親要素にスタイル指定をする方法について記事を書きました。

記事の中段でも書きましたが、
この :has() は、ReactやVue等、フロントエンドフレームワークが
複雑に組み込まれた動的サイトの案件においても、
要素の特定方法の一つとして、威力を発揮してくれる素晴らしい擬似クラスだと感じています。

現時点では主要ブラウザのうちFireFoxが未対応となっていますが、
Web上では「今年目標に対応されるのでは…?」というような噂も散見されますので、
今後の状況に期待したい、というところです。

現時点においても、JavaScriptによるアプローチを行うことで、同じ結果を得るための補完は可能ですので、
使い所をみきわめて、ぜひ柔軟に取り入れていきたいものですね。

この記事が皆さんのより良いWeb制作体験につながれば、嬉しく思います。

※この記事は内容の品質向上を目的に、随時更新を行う場合があります。

この記事をシェアする: