Plain hover: and focus: style the element being hovered or focused. Real interfaces often need more: a card whose title changes color when the whole card is hovered, or a label that turns red when the input beside it is invalid. Tailwind solves both with group and peer. In this lesson you will learn how they work, how to name them for nested layouts, and how has-* and in-* extend the idea.
Add group to the parent and group-<state>: to a descendant:
<article class="group relative overflow-hidden rounded-xl">
<img class="transition group-hover:scale-105" src="photo.jpg" alt="" />
<div class="absolute inset-0 flex items-end bg-black/0 p-4 group-hover:bg-black/40">
<span class="translate-y-2 opacity-0 transition group-hover:translate-y-0 group-hover:opacity-100">
View project
</span>
</div>
</article>Every state variant has a group form: group-hover:, group-focus:, group-focus-within:, group-active:, group-disabled:, group-open:, even group-first: and group-odd:. The generated selector is .group:hover .group-hover\:scale-105 — pure CSS, no JavaScript. group-focus-within: is ideal for search boxes: the wrapper gets a ring when the input inside is focused.
When groups nest, group-hover: reacts to the nearest one. To target a specific ancestor, name it with a slash and reuse the name in the variant:
<li class="group/item flex justify-between p-3 hover:bg-gray-50">
<span>Invoice #1042</span>
<a class="group/edit invisible group-hover/item:visible" href="#">
Edit <span class="group-hover/edit:translate-x-1 inline-block transition">→</span>
</a>
</li>The edit link appears when the row is hovered; the arrow moves only when the link itself is hovered. Names are arbitrary words that describe the component.
peer is group for siblings. Mark the element whose state matters with peer, then style any later sibling with peer-<state>::
<div class="relative">
<input id="email" type="email" placeholder=" " required
class="peer w-full rounded-md border px-3 pt-5 pb-2 invalid:border-red-500" />
<label for="email"
class="absolute top-1 left-3 text-xs text-gray-500 transition-all
peer-placeholder-shown:top-3.5 peer-placeholder-shown:text-base
peer-focus:top-1 peer-focus:text-xs peer-focus:text-blue-600">
Email address
</label>
<p class="mt-1 hidden text-sm text-red-600 peer-invalid:block">Enter a valid email.</p>
</div>This floating-label pattern needs no JavaScript: peer-placeholder-shown: detects an empty input, peer-focus: lifts the label, and peer-invalid: reveals the error. peer-checked: builds custom checkboxes and toggles. Named peers (peer/terms, peer-checked/terms:) disambiguate several inputs in one parent.
peer compiles to the sibling combinator ~, which only looks forward, so the peer element must come before the elements that react to it. If a design needs the label first, swap the visual order with flex-row-reverse.
Two newer variants cover cases that used to need group or peer:
| Variant | Meaning | Example |
|---|---|---|
| has-checked: | This element contains a checked input | has-checked:ring-2 on a label |
| has-[img]: | Contains a matching selector | has-[img]:p-0 on a card |
| in-focus: | Any ancestor is focused | Same as group-focus: without adding group |
| in-[.dark]: | Inside an element matching the selector | Theme-scoped styles |
has-* compiles to the :has() selector, so a <label class="has-checked:border-blue-600"> wrapping a radio button styles itself based on its own child. group-has-[…]: and peer-has-[…]: combine the two ideas.
peer in the DOM; sibling selectors only look forward.group class on the parent — the group-hover: variant silently does nothing.group-hover: only; add group-focus-within: for keyboard users.Which class must the parent have for `group-hover:text-blue-600` on a child to work?
group on an ancestor enables group-hover:, group-focus-within: and other group-* variants on descendants.peer on an earlier sibling enables peer-checked:, peer-invalid:, peer-placeholder-shown: and more on later siblings.group/name, group-hover/name:) when they nest or repeat.has-* styles an element based on its own children; in-* reacts to any ancestor without a group class.Next lesson: Structural, Data and ARIA Variants — target first and last children, nth-* positions, data-* and aria-* attributes, and pseudo-elements.