I spent an embarrassing amount of time last month staring at [&>*:nth-child(3n)]:mt-8 in a Blade template, trying to work out whether the 3n was the thing that had broken or the &>* was. That class had been copy-pasted through three projects since Tailwind v2.
It can all go. Tailwind v4 ships the whole nth-child family as core variants, and the tailwind nth-child variant is now boring in the best possible way. Every class in this article was compiled against tailwindcss@4.3.3 before I wrote about it, so the selectors below are what you actually get.
The four nth-child variant families and what they compile to#
Four variants, one for each pseudo-class. Pass the index directly after the variant name — there is no child in the class name, which is the first thing everyone gets wrong.
| Class | Generated selector |
|---|---|
nth-3:bg-red-500 |
.nth-3\:bg-red-500:nth-child(3) |
nth-last-2:bg-blue-500 |
.nth-last-2\:bg-blue-500:nth-last-child(2) |
nth-of-type-4:bg-green-500 |
.nth-of-type-4\:bg-green-500:nth-of-type(4) |
nth-last-of-type-6:bg-pink-500 |
.nth-last-of-type-6\:bg-pink-500:nth-last-of-type(6) |
They compose like any other variant. All of these compile:
<!-- responsive: the media query wraps the nth selector -->
<li class="md:nth-3:bg-red-500">…</li>
<!-- state stacking, either order, and they mean different things -->
<li class="hover:nth-3:bg-red-500">…</li> <!-- :hover:nth-child(3) -->
<li class="nth-3:hover:bg-blue-500">…</li> <!-- :nth-child(3):hover -->
<!-- group and peer both understand nth-* -->
<div class="group-nth-2:opacity-50">…</div>
<div class="peer-nth-2:opacity-50">…</div>
<!-- and it negates -->
<li class="not-nth-3:opacity-50">…</li> <!-- :not(:nth-child(3)) -->
That last one is worth a note. not-nth-3: gives you :not(:nth-child(3)), which is often the class you actually wanted — see styling the exception with the not-* variant for the wider pattern.
Arbitrary values and the "of S" form#
Bare numbers cover a single index. For An+B expressions, use brackets:
<div class="nth-[2n+1]:bg-gray-100">…</div> <!-- :nth-child(2n+1) -->
<div class="nth-[3n]:mt-8">…</div> <!-- :nth-child(3n) -->
<div class="nth-[4n+1]:col-span-2">…</div> <!-- :nth-child(4n+1) -->
<div class="nth-[-n+3]:font-bold">…</div> <!-- :nth-child(-n+3) — first three -->
<div class="nth-[n+4]:hidden">…</div> <!-- :nth-child(n+4) — fourth onwards -->
nth-4n+1: without brackets does not compile. Neither does nth-child-3:. If a class silently does nothing, check the brackets before you check anything else.
The of S clause needs one extra rule: underscores stand in for spaces, because a Tailwind class can't contain a space.
<!-- :nth-child(2n+1 of li) -->
<ul class="*:nth-[2n+1_of_li]:bg-yellow-100">…</ul>
<!-- :nth-child(2n of :not([hidden])) -->
<tbody class="*:nth-[2n_of_:not([hidden])]:bg-zinc-50">…</tbody>
That second one is the killer feature. Plain even: counts every row including the hidden ones, so filtering a table with [hidden] leaves you with two identically-coloured rows next to each other. of :not([hidden]) re-numbers the stripe as the rows disappear — the striping stays correct through a Livewire filter with zero JavaScript.
Where the variant applies: self versus children#
This trips up more people than the naming does. The variant applies to the element carrying the class, not its children.
So inside a Blade loop it just works, because the class is on the repeated element:
<tbody>
@foreach ($invoices as $invoice)
<tr class="nth-[2n_of_:not([hidden])]:bg-zinc-50 nth-last-1:border-b-0">
<td class="px-4 py-2">{{ $invoice->number }}</td>
<td class="px-4 py-2">{{ $invoice->total }}</td>
</tr>
@endforeach
</tbody>
But if you put the class on a static parent — a <ul> you wrote by hand, or a slot you don't control — you need the child variant *: in front:
<!-- ✗ the <ul> itself is not the third child of anything useful -->
<ul class="nth-3:mt-8">…</ul>
<!-- ✓ :is(… > *):nth-child(3) -->
<ul class="*:nth-3:mt-8">…</ul>
*:nth-3:mt-8 compiles to :is(.\*\:nth-3\:mt-8 > *):nth-child(3) — the child variant supplies the descendant hop that [&>*] used to. This is the same "style down from the parent" idea as reaching up from children with the has-* variant, just pointing the other way.
nth-child versus nth-of-type#
:nth-child counts every sibling. :nth-of-type counts only siblings of the same tag. They give identical results in a uniform list, which is why the difference stays invisible until the day it bites.
<div class="prose">
<h2>Heading</h2> <!-- child 1, h2 of-type 1 -->
<p>First paragraph</p> <!-- child 2, p of-type 1 -->
<p>Second paragraph</p> <!-- child 3, p of-type 2 -->
<p>Third paragraph</p> <!-- child 4, p of-type 3 -->
</div>
Want the second paragraph? *:nth-3: works today and breaks the moment someone adds a subheading. *:nth-of-type-2: targets the second <p> regardless of what else moves in around it. Rule of thumb: if the container holds mixed element types, reach for nth-of-type-*. If it's a table body or a loop output where every child is the same tag, nth-* is fine and reads better.
Patterns worth stealing#
Every fourth card spans two columns. A cheap way to break up a uniform grid without hand-authoring spans:
<div class="grid grid-cols-3 gap-4">
@foreach ($posts as $post)
<article class="nth-[4n+1]:col-span-2 rounded-lg border p-4">
{{ $post->title }}
</article>
@endforeach
</div>
If you need the inside of those cards to line up across the row as well, that's a job for subgrid rather than nth-*.
Staggered reveal. Fixed delays down a short list, no Alpine and no inline styles:
<ul class="*:transition-opacity *:duration-300">
<li class="nth-1:delay-0">One</li>
<li class="nth-2:delay-75">Two</li>
<li class="nth-3:delay-150">Three</li>
<li class="nth-4:delay-300">Four</li>
</ul>
For a list of unknown length this stops scaling — you can't write a delay class per index. That's the point at which I hand the stagger to a CSS custom property, or use starting-style and transition-discrete to animate the reveal itself and let the delays come from one variable.
Trim the last border. nth-last-1:border-b-0 compiles to :nth-last-child(1), which is :last-child the long way round. Use last:border-b-0 — shorter, same selector. nth-last-2: earns its keep when you want the second from the end.
Browser support and other gotchas#
The plain forms are safe everywhere. :nth-child(), :nth-last-child(), :nth-of-type() and :nth-last-of-type() are Baseline widely available and have been for years.
The of S clause is newer. Per the Chrome team's write-up it landed in Chrome and Edge 111, Firefox 113 and Safari 9 — so every evergreen browser has had it since mid-2023. It's safe for an app behind a login. If you support genuinely ancient browsers, the fallback is a plain even: stripe that's merely wrong-looking rather than broken.
only-child: is not a variant. The :only-child pseudo-class is spelled only: in Tailwind. only-child:hidden compiles to nothing at all — no error, no output, just a class that does nothing. Same family: first:, last:, only:, first-of-type:, last-of-type:, empty:.
Interpolated class names are invisible to the scanner. This does not work:
{{-- ✗ Tailwind never sees "nth-3:bg-red-500" as a string --}}
<li class="nth-{{ $i }}:bg-red-500">…</li>
The scanner reads your source as plain text; it does not evaluate Blade. Either write the classes out longhand, or safelist the range in CSS:
@import "tailwindcss";
@source inline("nth-{1,2,3,4,5}:delay-{0,75,150,300}");
Brace expansion generates the whole grid of utilities. There's more on the trade-offs — and how quickly this bloats your bundle — in safelisting dynamic Blade classes with @source inline().
odd:/even: are still the right default. If the pattern is genuinely every-other and nothing is being hidden, odd:bg-white even:bg-zinc-50 says what it means. nth-[2n+1]: is the same selector with more punctuation.
Wrapping Up#
Grep your Blade templates for [&>*:nth-child and [&>li:nth-child and delete every one of them — the core variant is shorter, scannable, and composes with group-*, peer-* and not-* in a way the arbitrary selector never did. If you're still mid-migration, the official Tailwind v3 to v4 upgrade tool handles the mechanical parts first.
The one class worth committing to memory is nth-[2n_of_:not([hidden])]: for any table you filter client-side. Pair it with data-* and aria-* variants and most of the state styling in a filterable table stops needing JavaScript at all.
FAQ#
Does Tailwind CSS support nth-child?
Yes. Since v4, :nth-child() has a first-class variant: nth-3: compiles to :nth-child(3). Before v4 you needed an arbitrary selector like [&:nth-child(3)]: or a community plugin, and those are what most search results still show you. The variant name has no child in it, which is the most common mis-guess.
How do I style every third element in Tailwind?
Use the arbitrary-value form nth-[3n]: on the repeated element — inside a Blade @foreach or a component loop, the class sits on each item and counts itself. If the class has to live on a static parent instead, prefix it with the child variant: *:nth-[3n]:mt-8. The bare nth-3: targets only the third element, not every third.
What is the difference between nth-child and nth-of-type in Tailwind?
nth-* counts every sibling in the container; nth-of-type-* counts only siblings with the same tag name. In a table body or a uniform loop they behave identically. In mixed content — a heading followed by paragraphs, say — nth-of-type-2: reliably targets the second <p> while nth-3: breaks as soon as another element is inserted above it.
How do I write nth-child(2n+1) in Tailwind?
Put the expression in square brackets: nth-[2n+1]:bg-gray-100 compiles to :nth-child(2n+1). Without the brackets it does not compile at all. If the expression contains spaces, such as the of S clause, replace each space with an underscore: nth-[2n+1_of_li]: becomes :nth-child(2n+1 of li).
Should I use odd:/even: or nth-* for striped table rows?
Use odd: and even: for a static table — they compile to :nth-child(odd) and :nth-child(even) and read far better than the bracketed equivalent. Switch to nth-[2n_of_:not([hidden])]: when rows can be hidden by a filter, because plain even: keeps counting the hidden rows and leaves two same-coloured rows adjacent.
Do I still need a nth-child plugin in Tailwind v4?
No. The four nth-* families are built into core, support arbitrary values including the of S clause, and compose with responsive prefixes, group-*, peer-* and not-*. Any nth-child plugin in your package.json is dead weight after a v4 upgrade — remove it and rewrite the classes as core variants.