Tailwind v4 nth-* Variants: Striped Tables and Staggered Lists Without a Plugin

The Tailwind nth-child variant is core in v4. See exactly what nth-3, nth-of-type-4 and nth-[2n+1_of_li] compile to, plus striped tables and staggered lists.

Steven Richardson
Steven Richardson
· 8 min read

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.

Steven Richardson
Steven Richardson

CTO at Digitonic. Writing about Laravel, architecture, and the craft of leading software teams from the west coast of Scotland.