skip to content

In CSS, how does the an+b formula in :nth-child() work, and why can p:nth-child(2) match nothing on a page full of paragraphs?

level: middleimportance: must knowfreq 70%

answer

  1. a formula, not a search
  2. substitute n = 0, 1, 2, …
  3. positions counted over all siblings
  4. the tag is a filter, not the counter
  5. of-type counts only same-tag siblings

basics

~20 s

:nth-child(an+b) substitutes n = 0, 1, 2, … and matches elements at the resulting 1-based positions among all siblings. The count ignores the rest of the selector, so p:nth-child(2) matches only when the second child happens to be a p.

solid answer

~50 s

The argument is a formula, not a search: the browser plugs in `n = 0, 1, 2, 3…` and every result that is a positive integer is a sibling position it matches, counting from 1. So `:nth-child(3n)` hits 3, 6, 9…, `:nth-child(3n+2)` hits 2, 5, 8…, and `:nth-child(-n+3)` hits only the first three, because n = 0, 1, 2 give 3, 2, 1 and anything larger goes to zero or negative. `odd` and `even` are shorthands for `2n+1` and `2n`. The trap is that the position is counted over *all* siblings, independent of the compound selector: `p:nth-child(2)` reads as "an element that is a `p` **and** is the second child of its parent". If a heading sits second, nothing matches. When you want "the second paragraph among the paragraphs", use `:nth-of-type(2)`, which counts only siblings of the same element type.

code

css · 14 lines
css
/* every third row, starting with the second */
tbody tr:nth-child(3n + 2) {
  background-color: #eef4ff;
}

/* the first three cards only */
.card:nth-child(-n + 3) {
  border-color: #333;
}

/* the last three children, whatever their tag */
.list > :nth-last-child(-n + 3) {
  opacity: 0.6;
}

go deeper

for a junior

Know that positions start at 1 and that :nth-child(odd) and :nth-child(even) do zebra striping. Be able to write :nth-child(3n) for every third element without hesitating.

for a middle

Explain the substitution of n = 0, 1, 2, … and why only positive results match. Say plainly that the index is counted over all siblings and that :nth-of-type() is the type-scoped alternative.

for a senior

Show judgment about brittleness: structural selectors break when a template injects a wrapper or hides rows, so know when of S or an explicit class on the element is the safer choice.

for a principal

Own the tradeoff between structural selectors and data-driven classes across a codebase — position-based styling is free and self-maintaining but couples CSS to DOM order, which is a real cost when markup is generated by many teams.

## What the formula means `:nth-child()` is a structural pseudo-class: it matches on an element's position among its siblings rather than on anything in its markup. Its argument is the microsyntax `an+b`, where `a` is a step and `b` is an offset, and both may be negative or omitted. The browser does not solve an equation. It substitutes `n = 0, 1, 2, 3, …` in turn, and every result that is a **positive integer** is a position that matches. Positions are 1-based — the first child is 1, not 0. ```css li:nth-child(3n) /* n=1,2,3 → 3, 6, 9, … */ li:nth-child(3n+1) /* n=0,1,2 → 1, 4, 7, … */ li:nth-child(3n+2) /* n=0,1,2 → 2, 5, 8, … */ li:nth-child(-n+3) /* n=0,1,2 → 3, 2, 1 — first three */ li:nth-child(n+4) /* 4, 5, 6, … — from the fourth on */ li:nth-child(5) /* a = 0 → exactly the fifth */ ``` `odd` and `even` are keywords equivalent to `2n+1` and `2n`. Because a compound selector may repeat a pseudo-class, you can intersect two formulas to express a range: `li:nth-child(n+3):nth-child(-n+6)` matches items 3 through 6. ## The count is over siblings, not over matches This is the part that surprises people. `:nth-child()` computes the element's index among **all** its element siblings — comments and text nodes do not count, but every element does, whatever its tag or classes. The rest of the compound selector is then applied as a separate filter. ```html <div> <p>one</p> <h2>heading</h2> <p>two</p> </div> ``` Here `p:nth-child(2)` matches nothing: child 2 is the `h2`, which is not a `p`. Read the selector as an *and*, never as "the second p". The same logic explains why `.item:nth-child(1)` can fail on a list whose first child is an unrelated wrapper element that a template injected. ## :nth-of-type and the type-scoped family `:nth-of-type()` takes the same `an+b` argument but counts only siblings **of the same element type**. In the markup above, `p:nth-of-type(2)` matches the second paragraph regardless of the heading between them. Note the asymmetry: type, not class. There is no built-in "nth element with this class" in the base syntax. The structural family also includes `:first-child`, `:last-child`, `:only-child`, and their type-scoped counterparts `:first-of-type`, `:last-of-type`, `:only-of-type`. `:only-child` is exactly `:first-child:last-child`. Counting from the end is done with `:nth-last-child()` and `:nth-last-of-type()`, which apply the same formula to positions numbered backwards — so `:nth-last-child(-n+3)` is "the last three children". ## The `of S` extension Selectors Level 4 adds `:nth-child(an+b of S)`, which first filters the sibling set down to those matching selector `S` and *then* applies the formula. This is genuinely different from putting `S` in the compound: ```css /* every 2nd row among the visible rows */ tr:nth-child(2n of :not([hidden])) { background: #f4f4f4; } /* rows that are BOTH visible AND at an even sibling position */ tr:not([hidden]):nth-child(2n) { background: #f4f4f4; } ``` The first keeps a clean stripe pattern when rows are hidden; the second develops gaps. The `of S` form shipped in Chrome 111 and Firefox 113 in 2023 (Safari has supported it far longer), so check your support floor before relying on it. ## Practical use "Style every third row starting from the second" is `:nth-child(3n+2)`. Zebra striping is `:nth-child(odd)`. Trimming the trailing separator of a list is `li:not(:last-child) { border-bottom: 1px solid; }`. All of these are recomputed automatically as the sibling set changes, which is why structural pseudo-classes beat baking index classes into templates. Two more things worth saying out loud in an interview. First, `a` may be negative only usefully in combination with a `b` large enough to yield positive results — `:nth-child(-2n+5)` gives 5, 3, 1. Second, whitespace in the formula is restricted: `3n + 1` is legal, `3 n+1` is not, and the sign must bind as `an+b` or `an-b`.

  • How would you select items 3 through 6 of a list without touching the markup?
    Chain two `:nth-child()` calls in one compound selector: `li:nth-child(n+3):nth-child(-n+6)`. The first bounds the range from below (3, 4, 5, …), the second from above (6, 5, 4, …), and a compound selector requires all its parts to match, so only 3–6 survive. Repeating a structural pseudo-class this way is legal and is the idiomatic range idiom.
  • Why is there no :nth-of-class(), and what do people use instead?
    Type-scoped counting was cheap for the engine because an element's type is fixed; class-scoped counting was not, so the family stopped at `:nth-of-type()`. The modern replacement is `:nth-child(an+b of .cls)`, which filters by any selector before counting. Where that support floor is too high, teams fall back to counting on the server or in the template.
  • Does :nth-child() count text nodes or comments between elements?
    No. Structural pseudo-classes operate on element siblings only, so stray whitespace, text, and comment nodes never shift the index. That is why hand-formatted markup and minified markup give identical results — a useful thing to state when someone blames indentation for a broken stripe pattern.

Think of the children as numbered seats in a row. :nth-child() calls out seat numbers; the element type in the selector only decides whether the person sitting there is the one you wanted.

saying these in an interview costs you the question

  • Says p:nth-child(2) means the second paragraph
  • Thinks indexing starts at zero like an array
  • Believes whitespace or comments shift the index
  • Claims :nth-of-type() counts by class
  • Reads -n+3 as an invalid or empty formula

context