If you know odd, even, and the basic an+b formula, you know enough to zebra-stripe a table. But :nth-child goes much further than that. This guide covers the parts that most tutorials skip: counting from the end with :nth-last-child, targeting elements by class with the CSS Level 4 of S syntax, combining multiple nth selectors for complex patterns, and the debugging process for when your selector stops matching.

If you need a refresher on the basics first, start with the CSS nth-child fundamentals guide.

:nth-last-child - counting from the end

:nth-last-child works identically to :nth-child except it counts backward from the last child instead of forward from the first. Every formula you know from :nth-child applies directly.

/* Last child (same as :last-child) */
li:nth-last-child(1) { color: #7c6af7; }

/* Second to last */
li:nth-last-child(2) { opacity: 0.7; }

/* Last 3 items */
li:nth-last-child(-n+3) { border-top: 1px solid #2d3148; }

/* Every other item counting from the end */
li:nth-last-child(odd) { background: #1a1d27; }

Key use case: :nth-last-child(-n+3) selects the last 3 items regardless of how many total items exist. You do not need to know the list length. This is the clean solution for "style the final few items differently" without JavaScript.

nth-last-child formula quick reference

The an+b formula works the same way for :nth-last-child as it does for :nth-child: only the counting direction changes. Use this table as a lookup when you need a formula fast.

FormulaSelectsCounting from the end
:nth-last-child(1)Last item only1st from last
:nth-last-child(2)Second to last2nd from last
:nth-last-child(-n+2)Last 2 items1st and 2nd from last
:nth-last-child(-n+3)Last 3 items1st, 2nd, 3rd from last
:nth-last-child(n+3)All except the last 23rd from last onward
:nth-last-child(odd)Every other item, from the end1st, 3rd, 5th from last...
:nth-last-child(3n)Every 3rd item, from the end3rd, 6th, 9th from last...

Combining nth-child and nth-last-child

You can chain both selectors on the same element to target items that satisfy both conditions at once. A classic use case: select only the middle items, excluding both the first and last.

/* All items except the first and last */
li:nth-child(n+2):nth-last-child(n+2) {
  border-left: 2px solid #2d3148;
}

/* All items except the first 2 and last 2 */
li:nth-child(n+3):nth-last-child(n+3) {
  opacity: 0.6;
}

/* Exactly the middle item of a 5-item list */
li:nth-child(3):nth-last-child(3) {
  font-weight: 700;
}

Quantity queries - styling based on how many items exist

One of the most powerful patterns with :nth-last-child is the quantity query: changing styles based on how many siblings an element has, without JavaScript. The trick combines :nth-last-child with :first-child.

/* Apply styles only when there are exactly 4 items */
li:nth-last-child(4):first-child,
li:nth-last-child(4):first-child ~ li {
  width: 25%;
}

/* Apply styles when there are 3 OR MORE items */
li:nth-last-child(n+3):first-child,
li:nth-last-child(n+3):first-child ~ li {
  font-size: 0.875rem; /* shrink text when list is long */
}

/* Apply styles when there are FEWER than 4 items */
li:nth-last-child(-n+3):first-child,
li:nth-last-child(-n+3):first-child ~ li {
  font-size: 1.25rem; /* larger text when list is short */
}

How it works: li:nth-last-child(4):first-child only matches if the element is simultaneously the first child AND 4th from the last - meaning there are exactly 4 items total. The general sibling combinator ~ then applies the same styles to all following siblings.

A simpler alternative: :has()

The :first-child plus :nth-last-child trick above works everywhere, but it is not easy to read at a glance. The :has() relational pseudo-class does the same job with a single, much clearer selector.

/* Apply styles once a list has 6 or more items */
ul:has(> li:nth-child(6)) {
  gap: 0.5rem;
}

/* Switch a grid to 4 columns once it has at least 4 children */
.grid:has(> :nth-child(4)) {
  grid-template-columns: repeat(4, 1fr);
}

Browser support: :has() is supported in Chrome 105+, Edge 105+, Firefox 121+, Safari 15.4+, and Opera 91+. It is not supported in Internet Explorer. For the widest possible support, the :first-child combination above still works as a fallback.

The CSS Level 4 "of S" syntax

Standard :nth-child counts all siblings regardless of type or class. CSS Selectors Level 4 introduced the of S syntax, which lets you count only elements that match a specific selector. This solves a long-standing limitation.

/* Without "of S": counts ALL children, not just .featured ones */
.card:nth-child(2) { /* matches .card only if it's the 2nd child overall */ }

/* With "of S": counts only .featured elements */
:nth-child(2 of .featured) {
  border-color: #7c6af7;
}

/* Every other .active item, ignoring .inactive siblings */
:nth-child(odd of .active) {
  background: #1a1d27;
}

/* Last 2 items that have the .highlight class */
:nth-last-child(-n+2 of .highlight) {
  font-weight: 700;
}

Browser support: The of S syntax is supported in Chrome 111+, Safari 9+, and Firefox 113+. It is not supported in Internet Explorer. Check current support before using it in production if you target older browsers.

Practical example: alternating card colors in a mixed list

Before of S, alternating styles on a filtered subset of elements required JavaScript or extra markup. Now it is pure CSS.

/* HTML structure:
   <ul>
     <li class="card">...</li>
     <li class="card featured">...</li>
     <li class="divider">...</li>
     <li class="card">...</li>
     <li class="card featured">...</li>
   </ul>
*/

/* Zebra-stripe only .card items, ignoring .divider */
:nth-child(odd of .card)  { background: #1a1d27; }
:nth-child(even of .card) { background: #141720; }

/* Highlight the 2nd featured card specifically */
:nth-child(2 of .featured) { border-color: #10b981; }

Negative values and what they actually do

Negative values in :nth-child are valid but confuse most developers. A negative a value does not select items from the end - that is what :nth-last-child is for. Instead, a negative a limits the selection to items up to a certain position.

/* -n+3 selects items 1, 2, 3 only (first 3) */
li:nth-child(-n+3) { font-weight: 600; }

/* How the formula resolves:
   n=0: -0+3 = 3 (matches)
   n=1: -1+3 = 2 (matches)
   n=2: -2+3 = 1 (matches)
   n=3: -3+3 = 0 (no match - counting starts at 1)
   n=4: -4+3 = -1 (no match)
*/

/* -n+5 selects items 1 through 5 */
li:nth-child(-n+5) { color: #7c6af7; }

/* Combine: items 3 through 7 only */
li:nth-child(n+3):nth-child(-n+7) { background: #1a1d2750; }

Selecting ranges, multiples, and the last two items

Three closely related questions come up constantly: how to select a range of items, how to select several unrelated positions at once, and how to select the last two items specifically. All three build on patterns already covered above, but they are worth spelling out directly.

A range of items

Pairing nth-child(n+X) with nth-child(-n+Y) on the same element selects a contiguous range, item X through item Y inclusive.

/* Select items 4 through 8, inclusive */
li:nth-child(n+4):nth-child(-n+8) {
  background: #1a1d27;
}

Multiple, non-contiguous positions

To target specific unrelated positions rather than a range, list each :nth-child selector separated by commas. This is different from the an+b formula, which only matches repeating patterns.

/* Highlight only items 2, 5, and 9 - no pattern connects them */
li:nth-child(2),
li:nth-child(5),
li:nth-child(9) {
  border-left: 3px solid #7c6af7;
}

Selecting the last two items

:nth-last-child(-n+2) selects the last two elements regardless of the total list length, the same way :nth-last-child(-n+3) selects the last three.

/* Select the last two elements */
li:nth-last-child(-n+2) {
  opacity: 0.5;
}

Advanced layout patterns

Magazine-style grid: first item full width, rest in columns

.grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1rem;
}

/* First item spans all 3 columns */
.grid-item:nth-child(1) {
  grid-column: 1 / -1;
}

/* Items 2 and 3 span 1.5 columns each (2 columns, side by side) */
.grid-item:nth-child(2),
.grid-item:nth-child(3) {
  grid-column: span 1;
}

/* Items 4 onwards take their natural 1/3 width */
.grid-item:nth-child(n+4) {
  grid-column: span 1;
}

Pricing table: highlight the middle tier

/* Works for any odd number of pricing cards */
.pricing-card:nth-child(2) {
  transform: scale(1.05);
  border-color: #7c6af7;
  z-index: 1;
}

/* For a dynamic number of cards, use nth-child + nth-last-child */
/* This targets the exact middle of any odd-count list */
.pricing-card:nth-child(odd):nth-last-child(odd):first-child ~ .pricing-card:nth-child(2) {
  border-color: #7c6af7;
}

Responsive grid with different first-row treatment

.card-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 1.5rem;
}

/* First row of 4 gets a larger image area */
.card-grid .card:nth-child(-n+4) .card-image {
  aspect-ratio: 16 / 9;
}

/* Remaining rows get square images */
.card-grid .card:nth-child(n+5) .card-image {
  aspect-ratio: 1 / 1;
}

/* Last row items: prevent orphan cards stretching full width */
.card-grid .card:last-child:nth-child(4n+1) { grid-column: 1 / 3; }
.card-grid .card:last-child:nth-child(4n+2) { grid-column: 2 / 4; }

Staggered animation delays with nth-child

@keyframes fadeSlide {
  from { opacity: 0; transform: translateY(12px); }
  to   { opacity: 1; transform: translateY(0); }
}

.list-item {
  animation: fadeSlide 0.3s ease both;
}

/* Each item delayed by 50ms more than the previous */
.list-item:nth-child(1)  { animation-delay: 0ms; }
.list-item:nth-child(2)  { animation-delay: 50ms; }
.list-item:nth-child(3)  { animation-delay: 100ms; }
.list-item:nth-child(4)  { animation-delay: 150ms; }
.list-item:nth-child(5)  { animation-delay: 200ms; }
.list-item:nth-child(n+6) { animation-delay: 250ms; } /* cap delay for long lists */

Striping table columns instead of rows

:nth-child is not limited to rows. Applying it to td instead of tr stripes columns, and combining it with a specific position highlights a single column, which is useful for calling out a "total" or "featured" column in a comparison table.

/* Stripe columns instead of rows */
td:nth-child(odd)  { background: #1a1d27; }
td:nth-child(even) { background: #141720; }

/* Highlight only the 3rd column, header and body cells */
th:nth-child(3),
td:nth-child(3) {
  background: #8d7df815;
  font-weight: 600;
}

Does :nth-child work in HTML email? (Outlook and Gmail)

If you build email templates, do not rely on :nth-child. According to Can I Email support data, :nth-child works in Apple Mail but is not supported in Outlook, Gmail, or Yahoo Mail. Outlook desktop renders email through the Word HTML engine rather than a browser engine, which is why so many modern CSS selectors and properties fail silently there.

Email-safe alternative: Add an explicit class to every row or item you want styled differently, such as class="row-alt" on alternating table rows, and target that class instead of a position-based selector. It is more markup, but it renders consistently across email clients.

Tailwind CSS: nth-child and nth-last-child variants

Tailwind CSS v4 added native nth-* and nth-last-* variants, so you no longer need an arbitrary-value hack for common cases.

<!-- Tailwind v4: native nth-* variants -->
<div class="nth-3:underline">...</div>
<div class="nth-last-5:underline">...</div>
<div class="nth-of-type-4:underline">...</div>

<!-- Arbitrary values for full an+b formulas, including "of S" -->
<li class="nth-[3n+1]:bg-slate-800">...</li>
<li class="nth-[2n+1_of_li]:font-bold">...</li>

On Tailwind v3 and earlier, there is no dedicated nth-* variant. Use an arbitrary variant with the & placeholder instead:

<!-- Tailwind v3: arbitrary variant fallback -->
<div class="[&:nth-child(3)]:bg-green-200">...</div>

Version check: Confirm your project's Tailwind version before choosing a syntax. The native nth-* variants only work on v4 and later; v3 projects need the arbitrary-variant syntax above.

Using :nth-child outside of stylesheets

:nth-child is a standard CSS selector, so anywhere an app accepts a CSS selector string, it works, not just inside a <style> block.

/* JavaScript: select every 3rd list item */
const items = document.querySelectorAll('li:nth-child(3n)');

/* Select the last two items */
const lastTwo = document.querySelectorAll('li:nth-last-child(-n+2)');

Browser automation and scraping tools that accept CSS locators, including Selenium and Playwright, accept :nth-child the same way. For a deeper look at using CSS selectors in JavaScript, Selenium, and scraping libraries, see the CSS selectors guide.

Debugging: why is nth-child not matching (or not working)?

This is the most-searched pain point with :nth-child. When your selector does not match, there are four common causes.

Cause 1: the element is not the expected child position

:nth-child counts all siblings inside the parent, including elements of different types. If there is a heading or a div before your list items, the count shifts.

/* HTML:
   <div>
     <h2>Title</h2>     <-- child 1
     <p>Intro</p>       <-- child 2
     <ul>
       <li>Item A</li>  <-- child 1 of ul
       <li>Item B</li>  <-- child 2 of ul
       <li>Item C</li>  <-- child 3 of ul
     </ul>
   </div>
*/

/* The li count resets inside <ul> - this works as expected */
li:nth-child(2) { color: #7c6af7; } /* targets "Item B" */

/* But this targets the 2nd child of div, which is <p>, not <li> */
div :nth-child(2) { color: #7c6af7; } /* targets the <p> */

Cause 2: mixing up nth-child and nth-of-type

p:nth-child(2) does not mean "the 2nd paragraph". It means "an element that is both a p AND the 2nd child of its parent". If the 2nd child is a div, the selector matches nothing - even if there is a p nearby.

/* HTML:
   <section>
     <h3>Heading</h3>   <-- child 1
     <p>First para</p>  <-- child 2
     <p>Second para</p> <-- child 3
   </section>
*/

/* WRONG: looks for a <p> that is child 1 - nothing matches */
p:nth-child(1) { font-weight: bold; }

/* CORRECT: targets the 1st <p> regardless of position */
p:nth-of-type(1) { font-weight: bold; }

/* ALSO CORRECT: targets the <p> that is child 2 */
p:nth-child(2) { font-weight: bold; }

Cause 3: whitespace text nodes (not in CSS, but worth knowing)

In JavaScript, whitespace between HTML tags creates text nodes that affect childNodes counts. CSS :nth-child ignores text nodes and counts only element nodes, so this is not a CSS issue - but it explains why your CSS selector works while a childNodes[1] call in JS gives unexpected results.

Cause 4: the selector specificity is being overridden

Your nth-child selector may be matching correctly but losing to a more specific selector elsewhere in your stylesheet. Check DevTools - if the property is shown with a strikethrough, it is being overridden, not failing to match.

/* This has lower specificity than a class selector */
li:nth-child(2) { color: #7c6af7; }  /* 0-1-1 */

/* This wins even if written earlier in the file */
.nav-item { color: #94a3b8; }         /* 0-1-0 -- actually lower, so nth-child wins here */

/* But this beats nth-child */
.nav .nav-item { color: #94a3b8; }    /* 0-2-0 -- wins */

Debugging workflow: Open DevTools, select the element, check the Styles panel. If your nth-child rule appears with a strikethrough, the selector matched but was overridden. If the rule does not appear at all, the selector is not matching - check child position and element type.

Performance with complex nth selectors

CSS selectors are evaluated right to left. A selector like .nav li:nth-child(odd) first finds all elements matching li:nth-child(odd) in the entire document, then filters for those inside .nav. For small DOMs this is irrelevant. For large lists with hundreds of items and complex nth expressions, overly broad selectors can slow rendering.

/* Slower: browser evaluates nth-child on ALL li elements first */
li:nth-child(3n+1) { color: #7c6af7; }

/* Faster: scoped to a specific container */
.results-list li:nth-child(3n+1) { color: #7c6af7; }

/* Best for very large lists: use a class instead */
.results-list .highlight { color: #7c6af7; }

Practical reality: For typical web pages with under a few thousand DOM nodes, nth-child performance is not a concern. Only optimise if you are building a data-heavy table or infinite scroll list and profiling shows selector matching as a bottleneck.

Frequently asked questions

Can I use nth-child with classes?

Not directly in CSS Level 3. .card:nth-child(2) selects an element that is both a .card and the 2nd child overall - it does not count only .card elements. For that behavior you need the CSS Level 4 of S syntax: :nth-child(2 of .card). If you need to support older browsers, JavaScript or adding index classes via HTML is the fallback.

Why does my nth-child selector work in CodePen but not my project?

The most common cause is a difference in HTML structure. In your project, there may be extra wrapper elements, comments, or sibling elements of different types that shift the child count. Inspect the actual parent element in DevTools and count the children directly to confirm the position number you expect.

Can I combine :not() with formulas other than -n+3?

Yes. :not() accepts any :nth-child formula, not just the basic first-three exclusion covered in the fundamentals guide.

/* All items except the first 3 */
li:not(:nth-child(-n+3)) { opacity: 0.6; }

/* All items except first and last */
li:not(:first-child):not(:last-child) { border-left: 1px solid #2d3148; }

/* Every 4th item that is not also the last */
li:nth-child(4n):not(:last-child) { border-bottom: 2px solid #7c6af7; }

Is there a :nth-child selector for the last N items?

Yes. Use :nth-last-child(-n+N). For example, li:nth-last-child(-n+4) selects the last 4 items in any list, regardless of total length. This is cleaner than using JavaScript to add a class to the last few items.

Does :nth-child work in Outlook and other email clients?

Not reliably. Per Can I Email's support data, :nth-child works in Apple Mail but is not supported in Outlook, Gmail, or Yahoo Mail. For HTML email templates, add explicit classes to alternating rows or items instead of relying on nth-child selectors.

Can I use nth-child in Tailwind CSS?

Yes. Tailwind CSS v4 includes native nth-* and nth-last-* variants, such as nth-3 and nth-last-5, plus arbitrary values like nth-[2n+1_of_li]. On Tailwind v3 and earlier, use an arbitrary variant instead, such as [&:nth-child(3)]:bg-green-200.

Can I combine :has() with nth-child?

Yes. :has() can replace the :first-child plus :nth-last-child quantity query trick with a much simpler selector, such as ul:has(> li:nth-child(6)) to style a list only once it reaches a certain size. :has() is supported in Chrome 105+, Edge 105+, Firefox 121+, and Safari 15.4+.

How do I select multiple, non-contiguous elements with nth-child?

Chain selectors with commas, such as li:nth-child(2), li:nth-child(5), li:nth-child(9), to target specific unrelated positions in a single rule. For a repeating pattern instead of individual positions, use the an+b formula, like nth-child(4n+1).

CSS nth-child Tester: Live Formula Preview

Write any :nth-child formula and instantly see which elements match - including advanced expressions like -n+4 and 3n+2.

Open nth-child Tester →