group & peer variants

[check: …]
📖 Pair this live Tailwind demo with the companion guide (.md) — this page is the rendered ground truth. ↗ frontend/tailwind: customization covers @custom-variant — this bundle covers the built-in group/peer system.

group-* vs peer-* — the two state channels

Both are ways to style one element based on another element's state — without JavaScript. group-* reaches downward: an ancestor carries group and any descendant can read its state. peer-* reaches forward: the trigger carries peer and only a LATER sibling can read it. The difference is just the CSS combinator Tailwind emits.

aspectgroup-* (ancestor → descendant)peer-* (sibling → sibling)
who wears the marker the ancestor: class="group" the trigger itself: class="peer"
who reads the state ANY descendant, at any depth only a following sibling sharing the same parent
compiled CSS combinator .group:hover .group-hover\:x (descendant ) .peer:checked ~ .peer-checked\:x (general sibling ~)
DOM direction downward only (parent → child, any level) forward only (older → younger sibling)
named form group/cardgroup-hover/card: peer/togglepeer-checked/toggle:
combine group-hover:peer-checked:bg-red-500 — parent hovered AND sibling checked

1 · hover the group / check the peer — both react live

Left: hover the card (or tab to its button) — the icon scales, the title brightens, an underline appears, and a CTA slides in. All children react to the SAME ancestor via group-hover: / group-focus-within:. Right: check the box — the label turns green, a panel slides open, and hidden text appears, all via peer-checked: driven by the checkbox (a previous sibling).

group · hover me

Lightning Card

all children react at once

The .group ancestor is the single source of state. Every child that uses group-hover: responds together — no per-element event listeners.

peer · check the box

Advanced panel unlocked. The checkbox — a previous sibling with class="peer" — is now :checked. CSS alone drives this: the label turned green, this panel slid in, and hidden text appeared below. No JavaScript.

group-cta opacity: — · peer-label bg: — · peer-panel max-h: —

2 · named groups/peers, stacking & the state menu

When groups/peers nest, name them to avoid ambiguity: group/card + group-hover/card: targets that specific ancestor only. Variants stack left-to-right as AND conditions.

patternwrites asmeaning
named group <div class="group/card">group-hover/card:opacity-100 react only when the card ancestor (not an outer group) is hovered
named peer <input class="peer/toggle">peer-checked/toggle:block react only to the toggle sibling (not a different peer in the same parent)
stacked (AND) group-hover:peer-checked:bg-red-500 parent hovered AND the sibling checkbox checked
group + peer group-focus-within:peer-invalid:border-red-500 ancestor has focus-within AND preceding sibling's input is invalid
arbitrary state group-data-[state=open]:block · peer-[:placeholder-shown]:opacity-50 data-* attribute or any pseudo-class/attribute on the trigger

Common states available as group-* / peer-*:

statevariantfires when the group/peer…
hovergroup-hover: / peer-hover:is pointed at
focusgroup-focus: / peer-focus:itself receives focus
focus-withingroup-focus-within: / peer-focus-within:or a descendant receives focus (keyboard-friendly hover proxy)
checkedgroup-checked: / peer-checked:checkbox/radio is checked
disabledgroup-disabled: / peer-disabled:form control is disabled
required / valid / invalidpeer-required: / peer-valid: / peer-invalid:form validation state changes
placeholder-shownpeer-placeholder-shown:input still shows its placeholder (empty)
data-*group-data-[size=lg]:carries a matching data attribute

intent → pattern

intentpatternwhy
style children from a parent's hover <div class="group"> + child group-hover:underline one ancestor = single source of state; many children react together
show a panel when a box is ticked <input class="peer"> + later sibling peer-checked:block pure-CSS disclosure — no JS, no details element
make hover keyboard-accessible too add group-focus-within: alongside group-hover: keyboard users who tab in see the same affordance
scope state to one nested group group/cardgroup-hover/card:opacity-100 avoids an outer group hijacking the inner card's children
combine two conditions group-hover:peer-checked:bg-red-500 variants stack as AND — read left-to-right
react to ANY custom state group-data-[open=true]: / peer-[:checked]: data attributes + arbitrary pseudo-classes both work
<div class="group hover:bg-cyan-600"> <p class="group-hover:underline">I underline when parent is hovered</p> </div> <input type="checkbox" class="peer" id="t"> <label for="t" class="peer-checked:bg-green-500">turns green when checked</label> <section class="group/card"> <input class="peer/like"> <button class="group-hover/card:peer-checked/like:bg-red-500">♥</button> </section>