😊

具体例で :has() の活用をイメージする

2023/12/18に公開

この記事は Cybozu Frontend Advent Calendar 2023 の 18 日目の記事です。

17 日目はこちら → GumTreeを使ってTypeScriptのファイルを跨いだ移動操作を検出する

こんにちは、最近は 一汁一菜の生活 を実践している yy616 です。

2023 年 12 月 19 日リリース予定の Firefox 121 で、:has() 擬似クラスがサポートされる予定となっており、主要ブラウザでのサポートが出揃います。そこで本記事では :has() の基本から実用例まで解説していきます。

:has() の基本

擬似クラス :has() はCSSセレクタの一種で、特定の子要素または子孫要素を持つ親要素を選択するために使用されます。CSSによる条件付きのスタイル適用を可能にし、JavaScriptに頼ることなく、より複雑なスタイルの条件を実現できます。

/* 子・孫要素の条件を指定して、親要素を選択する */
<target>:has(<condition>) { <styles> }

例えば、次のように使えば、h1要素を直接の子として持つdiv要素に青い境界線が適用されます。

div:has(> h1) {
  border: 1px solid blue;
}

留意点としては、:has() は他の :has() の中に入れ子にすることはできません。また、疑似要素は :has() 内では有効なセレクターではなく、:has() の有効なアンカーでもありません。

:has() はシンプルながらも、CSSの表現力を大幅に引き上げてくれます。以降ではもう少し実用的な例を見ていきます。

具体例

:has() を利用にすることで、従来では JavaScript を利用しないと実装できなかった仕組みをCSSだけで作れるようになります。本節では2つの実用例を見ながら使い方を確認していきます。

フォームのバリデーション

フォームの入力値の状態に合わせて、異なるスタイルを適用している例です。

form.html
<form>
  <div class="form-group">
    <label for="email" class="form-label">メールアドレス</label>
    <div class="form-group__input">
      <input
        required type="email" id="email" class="form-input"
        pattern="[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$"
        title="有効なメールアドレスを入力してください"
        placeholder="有効なメールアドレスを入力"/>
      <div class="form-group__error">有効なメールアドレスを入力してください</div>
    </div>
  </div>
</form>

入力エラーがあるフォームの色を変更する

form.css
.form-group:has(:invalid) {
  --color: var(--invalid);
}

.form-group内のどれかの要素が無効(:invalid状態)である場合に、その.form-groupにスタイルを適用します。このCSSではフォームの色を変更し、入力エラーがあることを示しています。

入力値が不正な際にエラーメッセージを表示する

form.css
.form-group:has(:invalid:not(:focus):not(:placeholder-shown)) .form-group__error {
  display: block;
}

.form-group内の入力要素が無効(:invalid)、フォーカスされていない(:not(:focus))、かつプレースホルダーが表示されていない(:not(:placeholder-shown))場合に、.form-group__error要素を表示(display: block)します。

つまり、ユーザーが入力フィールドからフォーカスを移動し、かつ無効な値が入力されている場合にエラーメッセージが表示されるようになります。

追従するホバー

ナビゲーションのリンクにホバーすると、下線のスタイルが追従する例です。

hover.html
<nav class="nav">
  <ul class="nav-list">
    <li><a href="/">ホーム</a></li>
    <li><a href="/services/">サービス</a></li>
    <li><a href="/projects/">プロジェクト</a></li>
    <li><a href="/contact/">お問い合わせ</a></li>
  </ul>
  <span class="nav-chaser"></span>
</nav>
hover.css
.nav:has(li:nth-child(1) > :where(a:hover, a:focus-visible)) {
  --distance: 0;
}
.nav:has(li:nth-child(2) > :where(a:hover, a:focus-visible)) {
  --distance: 1;
}
.nav:has(li:nth-child(3) > :where(a:hover, a:focus-visible)) {
  --distance: 2;
}
.nav:has(li:nth-child(4) > :where(a:hover, a:focus-visible)) {
  --distance: 3;

.nav-chaser が追従する下線で、変数 --distance が移動距離(translate)を管理しています。初期値は -1 なので、表示領域の外側に位置しています。

そして、何番目の項目がホバー、またはフォーカスされたかによって、この変数 --distance の値を変えることで移動距離が変化し、追従しているように見えるという仕組みです。

.nav:has(li:nth-child(X) > :where(a:hover, a:focus-visible)) というCSSセレクタは、ナビゲーションの特定のリンク(第X子要素にあるリンク)に対してユーザーがホバーやフォーカスを行った際に適用されるスタイルを指定します。

終わりに

擬似クラス :has() の登場により CSS のみで実現可能な表現が大幅に増えました。

この記事で紹介した使用例は、ほんの一部に過ぎません。アイデア次第で様々な実装に用いることができると思います。今後も便利な使用方法を見つけていきたいですね。

参考資料

https://developer.mozilla.org/ja/docs/Web/CSS/:has

https://griponminds.jp/blog/css-selectors-has-pseudo-class/

https://zenn.dev/takuyakikuchi/articles/1d5a3f3ec6fbdc#content

Discussion