CSSの届く範囲を広げる :has() 最終回 :has()を実践で使いこなす応用ユースケース

今回は、画像の有無に応じたカードレイアウト、連続する見出しのマージン調整、サブメニューへのアイコン追加といった、より実践的な3つのユースケースを通して、:has()の応用的な活用方法を紹介します。

発行

著者 坂巻 翔大郎 フロントエンド・エンジニア
CSSの届く範囲を広げる :has() シリーズの記事一覧
無料公開中
公開翌日4時まで

前回まで

前回は、モーダルダイアログ表示時のスクロールロックと、ホバー中のカード以外を暗くする演出という2つのユースケースを紹介しました。あわせて、:has()を使うときの注意点として、:has()の中に:has()をネストできないことと、引数に擬似要素を書けないことにも触れました。

第3回となる今回は、もう一歩進んで、より実践的な応用ユースケースを3つ取り上げます。いずれも、これまでJavaScriptやHTMLの作り込みで対応していた処理を、:has()を使うことでCSSだけで完結できるようになる例です。では、さっそく見ていきましょう。

ユースケース4:画像の有無でカードのレイアウトを切り替える

最初は、コンテンツの有無に応じてレイアウトを変える例です。

記事一覧やお知らせのリストで、「サムネイル画像があるカードは画像とテキストを横並びにし、画像がないカードはテキストだけで表示する」といった出し分けをしたい場面があります。これまでは、画像の有無をテンプレート側で判定して別々のクラスを付ける、といった対応が必要でした。

:has()を使えば、HTMLの構造は同じまま、imgをもつかどうかだけでレイアウトを切り替えられます。デモのスクリーンショットを見てみましょう。

カードのHTMLは、画像があるかないかの違いだけで、構造は共通です。

カードのHTML

<div class="card">
  <img src="thumbnail.jpg" alt="">
  <div class="card-body">
    <h2>画像ありのカード</h2>
    <p>本文...</p>
  </div>
</div>

<div class="card">
  <div class="card-body">
    <h2>画像なしのカード</h2>
    <p>本文...</p>
  </div>
</div>

CSSでは、imgをもつかどうかでレイアウトを分けます。

画像の有無でレイアウトを切り替える

/* 画像ありのカード:横並び2カラム */
.card:has(img) {
  display: grid;
  grid-template-columns: 1fr 2fr;
}

/* 画像なしのカード:1カラム */
.card:not(:has(img)) {
  display: grid;
  grid-template-columns: 1fr;
}

.card:has(img)で「imgを含むカード」を、.card:not(:has(img))で「imgを含まないカード」を選択しています。第1回で紹介した:not():has()の組み合わせが、ここで活きてきます。

HTMLの構造が同じでも、コンテンツの有無によってレイアウトを自動で調整できるため、テンプレート側でクラスを出し分ける手間がなくなります。CMSのように、画像があったりなかったりするコンテンツを扱う場面で特に便利です。

ユースケース5:連続する見出しのマージンを調整する

次は、組版やデザインでよく求められる、見出しのマージン調整です。

見出しの直後に、本文を挟まず別の見出しが続くことがあります。たとえば、h2のすぐ下にh3がくるようなケースです。このとき、見出し同士の間に通常の下マージンがそのまま入ると、間延びして見えてしまいます。見出しが連続するときだけ、間の余白を詰めたい、というわけです。

従来は、こうした箇所に専用のクラスを付けて対応していましたが、見出しの並び順は記事の内容によって変わるため、いちいちクラスを付けるのは面倒でした。:has()を使えば、「直後に別の見出しが続く見出し」を、構造から自動的に選択できます。

デモのスクリーンショットを見てみましょう。

CSSは次のとおりです。

連続する見出しのマージンを詰める

h2:has(+ h3),
h3:has(+ h4) {
  margin-bottom: 0.25em;
}

h2:has(+ h3)は、「直後にh3が続くh2」を選択しています。第1回で触れたように、:has()の引数に隣接兄弟結合子+を使うと、「前の兄弟」を選択できます。通常のCSSでh2 + h3と書くと、選択されるのはあとに続くh3のほうでしたが、h2:has(+ h3)なら手前のh2を選択できるわけです。

ここでは、選択した手前側の見出しのmargin-bottomを縮めることで、見出し同士の間隔を詰めています。h2の直後にh3がくる場合と、h3の直後にh4がくる場合の両方を、カンマでまとめて指定しています。

見出しが連続するか、本文が続くかは記事の内容次第ですが、:has()を使えば、どんな並びになってもCSS側で自動的に対応できます。

コラム:h2 + h3 で後続の見出し側を調整する手もある

h2:has(+ h3)の代わりに、h2 + h3で後続のh3側のmargin-topを調整する書き方も成立します。

後続の見出し側を調整する場合

h2 + h3,
h3 + h4 {
  margin-top: 0.25em;
}

ただし、この書き方には少し注意が必要です。スタイルを当てる対象がh3になるため、「なぜこのh3だけ上余白が違うのか」がセレクターを読んだだけではわかりにくくなります。また、h3margin-topはほかのルールでも調整されることがあり、意図せず競合する場合があります。

「連続している前の見出しの下余白を詰める」という意図からすれば、h2にスタイルを当てるh2:has(+ h3)のほうが、対象と意図が一致していて読みやすいといえます。

ユースケース6:サブメニューをもつナビゲーションにアイコンを追加する

最後は、ナビゲーションでよく見かける、サブメニューを示すアイコンの追加です。

グローバルナビゲーションなどで、サブメニュー(ドロップダウン)をもつ項目には「▼」のようなアイコンを付けて、「開けることを示したい」という場面があります。これまでは、サブメニューをもつ項目にだけ目印のクラスを付けたり、HTMLにアイコン用の要素を直接書き足したりしていたのではないでしょうか。

:has()を使えば、「サブメニューをもっている」という構造そのものを条件にできるため、HTMLを変更せずにCSSだけでアイコンを追加できます。まずはデモを見てみましょう。サブメニューをもつ項目にだけ、▼が付いています。

ナビゲーションのHTMLは、サブメニューをliの中に入れ子のulとしてもつ、一般的な構造です。

ナビゲーションのHTML

<nav>
  <ul>
    <li><a href="#">ホーム</a></li>
    <li>
      <a href="#">製品</a>
      <ul>
        <li><a href="#">ノートPC</a></li>
        <li><a href="#">デスクトップ</a></li>
      </ul>
    </li>
    <li><a href="#">会社情報</a></li>
  </ul>
</nav>

CSSでは、ulを直接の子にもつliの中のリンクにだけ、アイコンを付けます。

サブメニューを持つリンクに▼を付ける

nav li:has(> ul) > a::after {
  content: ' ▼';
  font-size: 0.75em;
}

nav li:has(> ul)で「直接の子としてulをもつli」を選択し、その直接の子であるa::after擬似要素にアイコンを追加しています。第1回で紹介した子結合子>:has()の引数にも使っています。> ulと書くことで、サブメニュー内にさらにネストされたulがある場合でも、直接のサブメニューをもつliだけに絞り込めます。

補足::has()の引数には擬似要素を書けない

前回触れたとおり、:has()の引数に::afterのような擬似要素は書けません。ここで使っている::afterは、:has()の引数の中ではなく、選択したa要素に対して付けているものなので問題ありません。:has()の引数の中に擬似要素を書く場合とは異なる、という点に注意してください。

サブメニューの有無は構造から判断できるため、項目を追加・削除したり、サブメニューを増やしたりしても、CSS側で自動的にアイコンの有無が切り替わります。HTMLに手を入れずにUIを改善できるのは、:has()ならではの利点です

まとめ

今回は、:has()の応用的なユースケースを3つ紹介しました。おさらいしておきましょう。

  • 画像の有無に応じたカードレイアウトの切り替え::has(img):not(:has(img))で出し分ける
  • 連続する見出しのマージン調整::has(+ h3)で「前の見出し」を選択する
  • サブメニューをもつ項目へのアイコン追加::has(ul)で構造を条件にする

これらに共通するのは、これまでJavaScriptやテンプレート側のクラス出し分け、HTMLの作り込みで対応していた処理が、:has()によってCSSだけで、しかも構造の変化に自動で追従するかたちで書ける、という点です。

本シリーズでは、:has()の基本から応用までを3回にわたって解説しました。第1回・第2回で紹介した身近なユースケースと、今回の応用ユースケースを通して、:has()が実装の幅を大きく広げてくれることが、おわかりいただけたかと思います。

「特定の要素をもつ要素」を選択するという発想は、一度身につけると、さまざまな場面で応用が利きます。ぜひ実際のプロジェクトでも活用してみてください。