child-index variants

[check: …]
📖 Pair this live Tailwind demo with the companion guide (.md) — this page is the rendered ground truth. ↗ Sibling bundle: group & peer variants — group/peer read element STATE; this bundle reads element POSITION in the DOM.

position-based variants — the full map

Each child-index variant compiles to a CSS structural pseudo-class. The browser matches these against the live DOM tree, so the styling re-evaluates automatically whenever siblings are added, removed, or reordered. No JavaScript, no per-item classes — write the rule once, it applies to whichever child happens to be in that position right now.

variantcompiles tomatches when…typical use
first: :first-child this element is the first child of its parent round top corners, remove top border
last: :last-child this element is the last child of its parent round bottom corners, remove bottom border
only: :only-child this element has no siblings (first AND last) full rounding when alone in a list
odd: :nth-child(odd) position 1, 3, 5, 7, … zebra striping row A
even: :nth-child(even) position 2, 4, 6, 8, … zebra striping row B
empty: :empty this element has NO children (no elements, no text) hide empty cells, style empty states
not-first: :not(:first-child) any child except the first add top margin/border to non-first siblings
not-last: :not(:last-child) any child except the last add bottom divider to non-last siblings
[&:nth-child(3)]: :nth-child(3) arbitrary — exactly the 3rd child highlight a specific position (no built-in nth-3:)

Key insight: Tailwind v4 ships first, last, only, odd, even, empty, and the not-* negations out of the box. There is no built-in nth-3: — for arbitrary positions use the arbitrary variant syntax [&:nth-child(3)]:, which emits the raw selector you write inside the brackets.

1 · live demo — zebra striping + first/last rounding

Every <li> below carries the same class string: odd:bg-slate-800 even:bg-slate-900 first:rounded-t-xl last:rounded-b-xl [&:nth-child(3)]:ring-2 [&:nth-child(3)]:ring-cyan-400. Add or remove items — the pseudo-classes re-evaluate automatically. The 1st item is always odd + first; the 3rd item is always odd AND gets the cyan ring (arbitrary nth-child(3)).

↑ When the list is empty, the <ul> itself matches :empty and the empty: utilities fire (dashed amber border, amber background). Remove all items to see it.

item count: — · [0] bg: — · [1] bg: — · [0] radius: —

2 · empty:hidden — hide an element with no children

The empty: variant is bidirectional: it can style an empty element (Panel 1 above) OR hide it. Here the card carries empty:hidden — when it has no children, it gets display:none and vanishes from the layout entirely. Toggle its content to watch it appear/disappear.

I am visible because this div has a child. Click below to empty me.

state: has 1 child node → visible

Real-world uses: hide empty table cells (<td class="empty:hidden">), hide empty error-message containers (<div class="empty:hidden" role="alert">{{ error }}</div>), show a fallback background on empty cards (empty:bg-slate-800).

intent → pattern

intentpatternwhy
zebra-stripe a list/table odd:bg-slate-800 even:bg-slate-900 alternating rows for readability — re-evaluates when items change
round only the outer corners of a list first:rounded-t-xl last:rounded-b-xl + UL overflow-hidden first/last child rounded; middle children stay square
style the sole item differently only:rounded-xl :only-child = first AND last at once (all corners)
add a divider between items (not after last) not-last:border-b negation form — cleaner than last:border-b-0
hide empty cells / empty containers empty:hidden removes the element from layout when it has zero children
style a specific position (not just odd/even) [&:nth-child(3)]:text-cyan-400 arbitrary variant — Tailwind has no built-in nth-3:
every 3rd item (modular grid) [&:nth-child(3n)]:text-cyan-400 arbitrary 3n expression for repeating patterns
first-of-type vs first-child [&:first-of-type]:font-bold arbitrary — Tailwind's first: is :first-child only
<!-- Zebra-striped list with first/last rounding --> <ul class="rounded-xl overflow-hidden border border-slate-700 empty:border-dashed empty:border-amber-500 empty:p-8"> <li class="px-4 py-3 odd:bg-slate-800 even:bg-slate-900 first:rounded-t-xl last:rounded-b-xl">row 1</li> <li class="px-4 py-3 odd:bg-slate-800 even:bg-slate-900 first:rounded-t-xl last:rounded-b-xl">row 2</li> <li class="px-4 py-3 odd:bg-slate-800 even:bg-slate-900 first:rounded-t-xl last:rounded-b-xl">row 3</li> </ul> <!-- Highlight exactly the 3rd child (no built-in nth-3:) --> <li class="[&:nth-child(3)]:ring-2 [&:nth-child(3)]:ring-cyan-400">3rd</li> <!-- Hide an element that has no children --> <td class="empty:hidden">{{ maybe_empty_value }}</td>