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.
| variant | compiles to | matches 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.
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
| intent | pattern | why |
|---|---|---|
| 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 |