Explain :nth-child() vs :nth-of-type() with an example where they would produce different results

Intermediate12 min interview
Skills tested:
Stating what each pseudo-class countsPredicting divergence in mixed-element markupReading an An+B formula, including negative valuesUsing the of S syntax to count filtered subsetsKnowing the reverse variants

Advertisement

🧩 Scenario

This pair is a reliable interview question because the intuitive reading of :nth-child is wrong. Developers write p:nth-child(2) meaning "the second paragraph", it works while the markup happens to contain only paragraphs, and it silently stops matching the day a heading is added at the top. Knowing which one counts what is the difference between a selector that survives markup changes and one that breaks invisibly.

Architecture Walkthrough

What Each One Counts

:nth-child(n) counts every element child of the parent, regardless of type, and then checks whether the matched element also satisfies the rest of the selector.

:nth-of-type(n) counts only siblings of the same element type, so it indexes within that type's sequence.

Read p:nth-child(2) as "a p that happens to be the second child of its parent", not "the second p". If the second child is an h2, the selector matches nothing at all, because there is no element that is both a p and the second child. Read p:nth-of-type(2) as "the second p among its p siblings", which is what people usually mean.

Where They Diverge

Take this markup:

<div>
  <h2>Heading</h2>
  <p>First</p>
  <p>Second</p>
  <p>Third</p>
</div>

p:nth-child(2) matches "First", because "First" is the second child of the div and it is a p.

p:nth-of-type(2) matches "Second", because among the three p siblings it is the second one.

p:nth-child(1) matches nothing, because the first child is an h2.

The divergence appears whenever siblings are of mixed types, which in real markup is almost always. The reason this bug survives review is that it works in the simple case: a container of nothing but <p> elements makes the two identical, and it stays identical right up until someone adds a heading, a wrapper, or a conditional element at the top.

Reading An+B

Both accept an An+B formula where n iterates over non-negative integers, and counting is 1-based, so :nth-child(1) is the first child, not the second.

  • 2n or even selects 2, 4, 6, …
  • 2n+1 or odd selects 1, 3, 5, …
  • 3n selects every third: 3, 6, 9, …
  • n+4 selects the fourth onward: 4, 5, 6, …
  • -n+3 selects the first three: as n goes 0, 1, 2, the results are 3, 2, 1, and further values are non-positive and therefore never match

The negative-A form is the one worth internalising, because -n+3 is the idiomatic way to say "the first three" and it looks backwards until you substitute values. Combining it with a positive form gives ranges: :nth-child(n+3):nth-child(-n+6) selects children three through six.

The of S Syntax

Selectors 4 adds :nth-child(An+B of S), which counts only elements matching the selector list S. This is genuinely different from chaining a class.

:nth-child(2 of .card) counts the .card elements and picks the second one. :nth-child(2).card counts all children, picks the second one, and then checks whether it happens to be a .card. In a list where non-card elements are interleaved, those give different answers, and only the first expresses "the second card".

This is what makes zebra striping work on filtered lists: :nth-child(odd of :not(.is-hidden)) stripes correctly even when rows are hidden, whereas :nth-child(odd) counts hidden rows and produces visibly broken alternation.

Note that :nth-of-type has no of S form, since its filter is fixed to the element type.

The Reverse Variants

:nth-last-child() and :nth-last-of-type() count from the end instead of the beginning. :nth-last-child(2) is the second-from-last child.

These enable a well-known technique: quantity queries. li:first-child:nth-last-child(3) matches only when a list has exactly three items, because the first child being simultaneously the third from last means the total is three. Combining that with a sibling combinator lets a layout change based on how many items it contains, which is otherwise impossible in CSS.

Also worth distinguishing: :first-of-type, :last-of-type, and :only-of-type are shorthands for the type-counting variants, while :first-child, :last-child, and :only-child count all siblings.

When to Use a Class Instead

Structural selectors couple styling to document structure. A selector that depends on an element being the third child breaks when a wrapper is introduced, when an element is conditionally rendered, or when a component is reused in a different container.

For genuinely structural intent, such as zebra striping or removing the border from the last row, they are the right tool and a class would be noise. For anything that is really a semantic distinction, such as "this card is featured", a class is more robust and self-documenting. The rule of thumb: use structural selectors for patterns based on position, and classes for patterns based on meaning.


Key Code Explained

/* Given:
   <div>
     <h2>Heading</h2>
     <p>First</p>
     <p>Second</p>
     <p>Third</p>
   </div>
*/

p:nth-child(2)   { color: red; }  /* matches "First"  — 2nd CHILD, and it is a p */
p:nth-of-type(2) { color: blue; } /* matches "Second" — 2nd P among p siblings   */
p:nth-child(1)   { color: green; }/* matches NOTHING  — the 1st child is an h2   */

/* An+B, 1-based counting */
tr:nth-child(even)   { background: #f8fafc; } /* 2, 4, 6, ... */
tr:nth-child(odd)    { background: #fff; }    /* 1, 3, 5, ... */
li:nth-child(3n)     { font-weight: 600; }    /* 3, 6, 9, ... */
li:nth-child(n + 4)  { display: none; }       /* the 4th onward */
li:nth-child(-n + 3) { display: block; }      /* the FIRST THREE */
/* -n+3: n = 0,1,2 -> 3,2,1; larger n gives non-positive, never matches */

/* A range: children 3 through 6 */
li:nth-child(n + 3):nth-child(-n + 6) { outline: 1px solid; }

/* of S counts a FILTERED subset — genuinely different from chaining */
:nth-child(2 of .card) { border-color: #3b82f6; } /* the 2nd CARD */
:nth-child(2).card     { border-color: #dc2626; } /* the 2nd CHILD, if it is a card */

/* The practical win: striping that survives hidden rows */
tr:nth-child(odd of :not(.is-hidden)) {
  background: #f8fafc;
}
/* tr:nth-child(odd) counts hidden rows and breaks the alternation */

/* Counting from the end */
li:nth-last-child(2)   { margin-bottom: 0; }
p:nth-last-of-type(1)  { margin-bottom: 0; } /* same as :last-of-type */

/* QUANTITY QUERY: matches only when there are EXACTLY three items */
li:first-child:nth-last-child(3),
li:first-child:nth-last-child(3) ~ li {
  width: 33.333%;
}

/* Type-counting shorthands vs all-sibling shorthands */
p:first-of-type  { font-size: 1.125rem; } /* first p, whatever precedes it */
p:first-child    { font-size: 1.125rem; } /* only if p is the first child   */

The three-selector block at the top is the whole answer and worth being able to reproduce with the markup. p:nth-child(2) matching "First" is the result that surprises people, and p:nth-child(1) matching nothing at all is the one that explains why the misunderstanding is dangerous rather than merely inaccurate: the selector does not fall back to something reasonable, it just stops applying.

The tr:nth-child(odd of :not(.is-hidden)) rule is the strongest argument for the of S syntax. Zebra striping on a filterable table is a real requirement, plain :nth-child(odd) counts rows the user cannot see, and before of S the only fixes were JavaScript reindexing or removing rows from the DOM entirely.


Tradeoffs

SelectorCountsMatches nothing whenSupports of S
:nth-child(n)All element siblingsThe nth child is a different type than requiredYes
:nth-of-type(n)Siblings of the same typeThere are fewer than n of that typeNo
:nth-last-child(n)All siblings, from the endSame as :nth-childYes
:nth-last-of-type(n)Same type, from the endSame as :nth-of-typeNo
:first-child / :last-childAll siblingsThe element is not first/last overallN/A
:first-of-type / :last-of-typeSame typeNo sibling of that type existsN/A

What Interviewers Actually Check

  • Whether you state clearly that one counts all siblings and the other only same-type siblings
  • Whether you can construct markup where they differ and predict both results
  • Whether you know p:nth-child(1) can match nothing rather than falling back
  • Whether you can read -n+3 and explain the negative coefficient
  • Whether you know the of S syntax and how it differs from chaining a class

Follow-Up Questions

  1. Why does :nth-of-type have no of S form?
  2. How does a quantity query such as li:first-child:nth-last-child(3) work, and what are its limits?
  3. Does :nth-child count text nodes or comments, and why does that matter for whitespace in markup?
  4. What specificity does :nth-child(2 of .card) have, given that of S takes a selector list?
  5. When is a structural selector the wrong tool, and what makes a class more robust?

Common Candidate Mistakes

  • Reading p:nth-child(2) as "the second paragraph", when it means "a p that is the second child" and matches nothing if the second child is another element type
  • Chaining a class as in :nth-child(2).card and expecting it to count only cards, when it counts all children first and then tests the class
  • Misreading the An+B formula, particularly -n+3, which selects the first three rather than something before the third
  • Forgetting the counting is 1-based, so :nth-child(1) is the first child rather than the second
  • Using structural selectors for distinctions that are really semantic, producing rules that break when a wrapper is added or an element is conditionally rendered

Interview Readiness Checklist

Before you leave this question, make sure you can answer:

  • Can you state exactly what each pseudo-class counts?
  • Can you construct markup where the two produce different results, and predict both?
  • Can you read and write An+B formulas including -n+3 and a bounded range?
  • Can you use of S and explain how it differs from chaining a class?
  • Can you name the reverse variants and give a real use for them?

Summary

:nth-child() counts every element child of the parent and then checks the rest of the selector against the matched element. :nth-of-type() counts only siblings of the same element type. So in a div containing an h2 followed by three paragraphs, p:nth-child(2) matches the first paragraph, p:nth-of-type(2) matches the second, and p:nth-child(1) matches nothing at all because the first child is an h2.

That last case is why the misunderstanding is dangerous. The selector does not degrade to something sensible; it stops applying entirely, and it only starts failing when someone adds a heading or a wrapper to markup that previously contained a single element type. Both accept an An+B formula with 1-based counting, where even and odd are the common cases and -n+3 is the idiomatic way to express "the first three" because substituting n = 0, 1, 2 yields 3, 2, 1.

Selectors 4 adds :nth-child(An+B of S), which counts only elements matching S and is genuinely different from chaining a class: :nth-child(2 of .card) finds the second card, while :nth-child(2).card finds the second child and tests whether it is a card. That distinction is what makes zebra striping work on a filtered table, where :nth-child(odd) would count hidden rows. The :nth-last-child() and :nth-last-of-type() variants count from the end and enable quantity queries such as li:first-child:nth-last-child(3) for "exactly three items". Reach for these when the pattern is genuinely positional and for a class when it is semantic.

Frequently Asked Questions

Why does p:nth-child(2) sometimes match nothing?

Because it means "a p that is the second child of its parent". If the second child is a different element type, nothing matches. nth-of-type counts only p siblings, so it always finds the second one if it exists.

Can nth-child filter by class?

Yes, with the of S syntax: :nth-child(2 of .card) counts only elements matching .card. Plain :nth-child(2).card counts all siblings first and then checks the class.

Advertisement


Stay Updated

Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.

Advertisement