Group and peer variants
The usual hover: reacts to hovering over the element itself. Often you need something else: hover the whole card and the button inside changes; check a box and the label next to it changes. That is what the group-* variants (parent state) and peer-* variants (sibling state) are for.
group: the parent's state
The parent gets the group class, and the children get group-hover:, group-focus: and other variants:
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<a href="#" class="group block max-w-xs rounded-xl border border-gray-200 p-4 hover:border-indigo-400">
<h2 class="font-semibold group-hover:text-indigo-600">Wireless headphones</h2>
<p class="text-sm text-gray-500">More about this product</p>
</a>Hover anywhere on the card — the name turns blue, even if the cursor is over the description.
A product card with group
The image zooms in a little and the button appears while the cursor is over the card:
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<div class="group max-w-xs overflow-hidden rounded-xl bg-white shadow-lg">
<div class="relative overflow-hidden">
<img src="/media/photo-400x250.jpg" alt="Headphones" class="h-44 w-full object-cover transition group-hover:scale-105">
<button class="absolute right-3 bottom-3 rounded-lg bg-white px-3 py-1 text-sm font-medium opacity-0 shadow group-hover:opacity-100">
Add to cart
</button>
</div>
<p class="p-4 font-semibold">Headphones — $79</p>
</div>transition makes the zoom smooth — transitions are the next lesson.
Named groups
When groups are nested, group-hover: reacts to any of them. A group can be named: group/item, and the child gets group-hover/item:visible. Then the style depends only on the ancestor you mean.
peer: a sibling's state
peer goes on one element, and a peer-* variant on a sibling that comes after it. The most common case is an error hint next to a field:
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<label class="block max-w-xs">
<span class="text-sm font-medium">Email</span>
<input type="email" class="peer mt-1 w-full rounded-lg border border-gray-300 px-3 py-2" placeholder="anna@example.com">
<span class="mt-1 hidden text-sm text-red-600 peer-invalid:block">Enter a valid address</span>
</label>Type some text without an @ sign — a red hint appears below the field. peer-invalid:block shows it when the sibling field fails the browser's validation.
peer-checked: for checkboxes and switches and peer-focus: for field labels work the same way.
Common mistake
Putting the element with peer-* before the peer element:
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<label>
<span class="text-gray-500 peer-checked:text-green-600">Subscribe to news</span>
<input type="checkbox" class="peer">
</label>Check the box — the label does not turn green. peer-* is built on the ~ sibling selector, which only sees following elements. Put the element with the peer class earlier in the markup.