「このカードの中のチェックボックスがオンになったら、カードごと枠線を青くしたい」——こういう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-* だけ矢印の向きが逆だとわかります。

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

フォームまわりは 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の違い を先に読むと、この記事の内容がより腑に落ちるはずです。
スポンサーリンク
