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.

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.

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; }

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 */

Debugging: why is nth-child not matching?

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

What is the difference between :nth-child and :nth-of-type?

:nth-child counts all sibling elements regardless of type. :nth-of-type counts only siblings of the same element type. Use :nth-child when your list contains only one element type (like a ul full of li elements). Use :nth-of-type when siblings of different types are mixed and you want to count only one specific type.

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.

Does :nth-child(0) select anything?

No. :nth-child counts from 1. :nth-child(0) never matches any element. This is a common mistake when developers assume zero-based indexing from JavaScript carries over to CSS.

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 :nth-child with :not?

Yes, and it is useful. li:not(:nth-child(-n+3)) selects all list items except the first three. li:not(:first-child):not(:last-child) selects all middle items. These combinations work in all modern browsers.

/* 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.

Test any nth-child expression live

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

Open nth-child Tester →