Tailwind CSSのhas-*入門|group-has / peer-hasで「子の状態」から親を装飾する(JSなし)

group/peerは子や兄弟が親・兄弟の状態を見るのに対し、has-*は親が子孫の状態を見るという矢印の向きの違いを示した図
  • このエントリーをはてなブックマークに追加

スポンサーリンク

「このカードの中のチェックボックスがオンになったら、カードごと枠線を青くしたい」——こういうUI、ついJavaScriptで addEventListener を書いてしまっていませんか?

Tailwind CSSには group や peer という仕組みがあります。ただ、どちらも「子や兄弟が、親・兄弟の状態を見る」方向の機能でした。その逆、つまり「親が、中の子要素の状態を見て自分を装飾する」のが今回の has-* です。

この記事では、has-* の基本から、group-has-*・peer-has-* との組み合わせ、フォームの検証状態でカードの枠線を変える実例、そして「効かない!」ときのチェックリストまで、コピペできるコードで解説します。Tailwindの導入がまだの方は RailsにTailwind CSS v4を導入する方法 から、全体像をつかみたい方は Tailwind CSSの使い方【初心者向け】 も合わせてどうぞ。

まず結論:group/peerは「子が親を見る」、has-*は「親が子を見る」

細かい話に入る前に、いちばん大事なところだけ先に言ってしまいます。3つの機能は「だれの状態を、どこに伝えるか」の向きが違うだけです。

  • group:親の状態を、その中の子要素が参照する(親→子)
  • peer:先に書いた兄弟の状態を、後ろの兄弟が参照する(兄弟→兄弟)
  • has-*:中の子孫の状態を、親自身が参照する(子→親)

図にすると、has-* だけ矢印の向きが逆だとわかります。

group/peerは子や兄弟が親・兄弟の状態を見るのに対し、has-*は親が子孫の状態を見るという矢印の向きの違いを示した図
group/peer は「子・兄弟が外を見る」、has-* は「親が中を見る」。向きが逆になる

has-* の正体は、CSSの :has()(通称「親セレクタ」)です。長らくCSSには「子の状態で親を選ぶ」手段がなく、JavaScriptで補うしかありませんでした。それが :has() で可能になり、Tailwindからは has-* で使えます。group と peer そのものの違いは Tailwind CSSのpeerとは?groupとの違い に詳しくまとめています。

has-*の基本:中のチェックで「コンテナ」の色を変える

いちばんシンプルな例から始めます。コンテナ側(親)に has-[:checked] を付けると、その中のどこかに :checked な要素があるときだけ、親自身のスタイルを切り替えられます。

<label class="block rounded-lg border-2 border-slate-200 p-4
              has-[:checked]:border-indigo-500 has-[:checked]:bg-indigo-50">
  <input type="checkbox" class="mr-2" />
  メール通知を受け取る
</label>

ポイントは、状態を見たい親(ここでは label)に has-[...] を付けることです。角カッコの中には、CSSのセレクタをそのまま書きます。has-[:checked] は「:checked な子孫を持つなら」という意味です。

これで、チェックを入れた瞬間に label ごと枠線と背景が変わります。JavaScriptは一行も書いていません。従来は「チェック状態を親に伝えるために、わざわざJSでクラスを付け外しする」必要があったことを考えると、かなり短く書けます。

よく使うhas-*の書き方(任意値が基本)

has-[...] の角カッコには、要素名でも擬似クラスでも、組み合わせでも書けます。まずはこの「任意値」の形を覚えれば、たいていの場面に対応できます。

書き方意味(こういう子孫を持つ親に効く)
has-[:checked]チェック/選択された要素
has-[:focus]フォーカスされている要素
has-[:invalid]入力が不正な要素(バリデーション)
has-[img]画像(img要素)
has-[input:focus]フォーカス中の入力欄
has-[>_img]直下に画像を持つ(> img)

最後の has-[>_img] に注目してください。セレクタにスペースを入れたいときは、アンダースコア _ で表すのがTailwindのルールです。> img と書きたいところを >_img と書くと、ビルド時に :has(> img) へ変換されます。ここは最初に戸惑いやすいポイントです。

なお、よく使う状態には has-checked: のような短縮形も用意されています。ただし覚えることを増やさないなら、まずは has-[:checked] の任意値の形に統一しておけば確実です。

group-has-*:深い階層の子孫の状態で親を装飾する

has-* は「自分の子孫」を見ますが、「特定の祖先」を狙って装飾したいときは group と組み合わせます。祖先に group を付け、反応させたい要素に group-has-[...] を付ける形です。

<div class="group rounded-lg border p-4">
  <div class="flex items-center justify-between">
    <span>詳細オプション</span>
    <!-- このチェックの状態を、group全体が参照する -->
    <input type="checkbox" />
  </div>

  <!-- 普段は隠し、group内のどこかがcheckedのときだけ表示 -->
  <div class="hidden group-has-[:checked]:block mt-3 text-sm text-slate-600">
    詳細設定の入力欄がここに現れます
  </div>
</div>

素の has-* との違いは、反応する要素と状態を持つ要素が離れていてもよい点です。group を付けた祖先の配下であれば、何階層ネストしていても group-has-* で拾えます。アコーディオンの開閉やオプション欄の出し分けに向いています。group を入れ子で使うテクニックは Tailwind CSSのgroup-hover完全解説(ネスト) も参考にしてください。

peer-has-*:兄弟の中の状態で「後続の要素」を装飾する

親ではなく横並びの兄弟に伝えたいときは peer-has-* です。状態を持つ側(の親)に peer を付け、後ろの兄弟に peer-has-[...] を付けます。

<fieldset class="peer">
  <label><input type="radio" name="plan" /> 有料プラン</label>
</fieldset>

<!-- 直前のpeerの中が選択されたら、この注意書きを表示 -->
<p class="hidden peer-has-[:checked]:block text-sm text-amber-600">
  有料プランには月額料金が発生します
</p>

peer は「先に書いた兄弟の状態を、後ろの兄弟が見る」仕組みなので、HTML上で peer が先・反応する要素が後ろという順序が必須です。ここは peer 単体と同じ制約です。順序を逆にすると効かないので注意してください。

実例:フォームの検証状態でカードの枠線色を変える

has-* がいちばん活きるのがフォームです。入力欄の :invalid / :valid を親カードが見れば、入力の正誤でカードの枠線色をまとめて切り替えることができます。

<label class="block rounded-lg border-2 border-slate-200 p-4
              has-[:focus]:border-indigo-400
              has-[:valid]:border-emerald-500
              has-[:invalid]:border-rose-500">
  <span class="mb-1 block text-sm font-bold">メールアドレス</span>
  <input type="email" required
         class="w-full rounded border border-slate-300 px-2 py-1" />
</label>

入力欄そのものではなく、ラベルやカード全体の見た目を状態で変えられるのがポイントです。エラー時に枠だけでなくカードの背景やアイコンもまとめて切り替えたい、といった要望に一箇所で応えられます。

入力欄の検証状態に応じて、親カードの枠線がフォーカス時は青、正しい入力は緑、不正な入力は赤に変わる3つの状態の比較図
入力欄の :focus / :valid / :invalid を親カードが has-* で参照し、枠線色を切り替える

フォームまわりは peer と役割が近い場面もあります。「エラーメッセージを兄弟として出し分けたい」なら peer-invalid:、「入力欄を含むカードごと装飾したい」なら has-[:invalid]、と覚えると迷いません。フォームの基本的な組み方は Tailwind CSSでフォームを綺麗に作る方法 にまとめています。

効かないときのチェックリスト

has-* は仕組みがシンプルなぶん、つまずくポイントも決まっています。「効かない!」ときは上から順に確認してください。

  • セレクタのスペースをアンダースコアにしているか:has-[> img] ではなく has-[>_img]。角カッコ内のスペースはそのままだとクラスとして認識されません
  • 状態を見る「親」側にクラスを付けているか:has-* は親に、group-has-* は group を付けた祖先の配下に、peer-has-* は peer の後続に、と付ける場所が違います
  • クラス名を文字列連結で動的生成していないか:Tailwindはソース中のクラス名を静的に探すため、has-[:checked] のように完全な形で書く必要があります(data属性で状態を切り替える方法 も同じ注意点です)
  • 引用符のエスケープ忘れ:has-[[data-state='open']] のように属性値を含む場合、HTML側の引用符と衝突しないよう注意します

ブラウザ対応も一応押さえておきましょう。:has() はChrome / Edge 105(2022年)、Safari 15.4(2022年)、Firefox 121(2023年12月)で出そろい、2024年以降は主要ブラウザすべてで使えます。よほど古い環境を相手にしない限り、実務で気にする必要はほぼありません。

group-has-* と peer-has-* の使い分け早見表

最後に、3つのどれを使うか迷ったとき用の早見表です。「反応させたい要素」と「状態を持つ要素」の位置関係で選びます。

使うもの状態を持つ要素装飾される要素向いている場面
has-[...]自分の子孫自分自身(親)中のチェック/入力でカード自体を装飾
group-has-[...]group配下のどこか同じgroup内の別の子孫離れた位置の要素を出し分け(アコーディオン等)
peer-has-[...]peerの子孫peerより後ろの兄弟選択に応じて後続の注意書きを表示

まとめ

has-* は、これまでJavaScriptでしか書けなかった「子の状態で親を装飾する」をCSSだけで実現する機能です。ポイントを3つに絞ると次のとおりです。

  • has-* は「親が子孫を見る」。group/peer とは向きが逆
  • 基本は has-[:checked] のような任意値の形で書く。スペースは _ に
  • 離れた要素には group-has-*、後続の兄弟には peer-has-*

フォームのバリデーション表示やオプションの出し分けなど、「状態に応じて見た目を変える」UIがぐっと短く書けます。まずは has-[:checked] でカードの色を変えるところから試してみてください。group と peer の基礎が怪しい方は peerとgroupの違い を先に読むと、この記事の内容がより腑に落ちるはずです。

スポンサーリンク

  • このエントリーをはてなブックマークに追加