Really understanding and applying the An+B math
Most developers only know nth-child through odd and even, yet behind it sits a complete linear formula that enables far more complex patterns. This collection explains the An+B math, nth-last-child and the of S syntax through concrete recipes for grids, lists and pagination.
Table of Contents
- 1. Why odd and even are only the surface of nth-child
- 2. The An+B formula explained step by step
- 3. Recipe: selecting ranges, excluding the first and last N
- 4. nth-last-child: using backward counting correctly
- 5. The of S syntax: understanding filtered counting
- 6. Recipe: grid layouts with row break logic
- 7. nth-child vs. nth-of-type: the decisive difference
- 8. Recipe: pagination groups and separator lines
- 9. nth-child formulas compared directly
- 10. Summary
- 11. FAQ
1. Why odd and even are only the surface of nth-child
Most tutorials reduce the nth-child pseudoclass to two keywords: odd for odd and even for even positions, usually demonstrated with zebra striped table rows. Those two keywords, though, are just named shortcuts for the actual, considerably more powerful syntax behind them, a linear formula of the form An+B that can express practically any repeating pattern across a list of siblings.
Anyone who only knows odd and even usually reaches for JavaScript or extra classes in the markup for more complex requirements, such as every third element starting from the fifth, or every element except the last two. With nth-child and the underlying formula, nearly any numeric position condition can be expressed directly in the selector. The following sections show how the formula works and which practical patterns can be derived from it.
2. The An+B formula explained step by step
The formula behind nth-child is An+B, where n stands for every non negative integer starting at zero, A determines the step size between matches, and B adds an offset. nth-child(3n+1) computes the positions 1, 4, 7, 10 and so on for n=0,1,2,3..., because three is added to the previous position each time, starting at one. Understanding this calculation is the key to using nth-child for arbitrary patterns instead of relying on premade keywords.
Negative values for A are also valid and effectively reverse the counting direction: nth-child(-n+3) computes the positions 3, 2, 1 and stops there because negative positions do not exist, which in practice selects exactly the first three elements. This property makes -n+X one of the most useful building blocks for range selection with nth-child, far more compact than a long enumeration of individual positions.
/* A = 3, B = 1: positions 1, 4, 7, 10, ... */
li:nth-child(3n+1) {
font-weight: bold;
}
/* Negative A effectively selects "the first N" elements */
li:nth-child(-n+3) {
color: #7c3aed;
}
/* A = 0 behaves like an exact position (equivalent to nth-child(5)) */
li:nth-child(0n+5) {
background: #f5f3ff;
}
/* Classic odd/even are just named shortcuts for 2n+1 and 2n */
tr:nth-child(odd) { background: #f8fafc; } /* same as 2n+1 */
tr:nth-child(even) { background: #ffffff; } /* same as 2n */
3. Recipe: selecting ranges, excluding the first and last N
A common use case is selecting a range inside a list, for example every element between position four and eight. With nth-child, this can be solved by combining two formulas in one selector: li:nth-child(n+4):nth-child(-n+8) matches exactly positions four through eight, because the first formula captures everything from position four onward and the second formula limits it to position eight. The intersection of both conditions yields the desired range.
A second common pattern is excluding the first and last elements of a list, for example when separator lines should appear between entries but not before the first or after the last entry. li:not(:first-child):not(:last-child) solves this directly, while li:nth-child(n+2) alone already excludes the first element when no additional endpoint is needed. These range patterns with nth-child replace many cases that would otherwise require an extra CSS class on the first or last markup element.
/* Select exactly positions 4 through 8 */
li:nth-child(n+4):nth-child(-n+8) {
background: #ede9fe;
}
/* Separator lines between items, but not before the first or after the last */
li:not(:first-child) {
border-top: 1px solid #e2e8f0;
}
/* Fade out the last three items in a preview list */
li:nth-last-child(-n+3) {
opacity: 0.5;
}
4. nth-last-child: using backward counting correctly
nth-last-child uses the same An+B math as nth-child, but counts from the end of the sibling list instead of the beginning. That makes it the ideal tool for patterns that depend on the total number of elements without explicitly knowing that number. li:nth-last-child(-n+3) always selects the last three elements, regardless of whether the list contains five or fifty entries in total, because counting always happens relative to the end.
A particularly elegant pattern combines nth-child and nth-last-child to find the exact middle element of a list, regardless of its length: li:nth-child(odd):nth-last-child(odd) only matches lists with an odd total length and automatically marks the middle element there, because only the exact middle has an odd position both from the front and from the back. This technique works entirely without JavaScript and without knowing the list length in advance.
/* Always the last three items, regardless of total list length */
li:nth-last-child(-n+3) {
border-color: #7c3aed;
}
/* Find the exact middle element of an odd-length list */
li:nth-child(odd):nth-last-child(odd) {
font-weight: 700;
color: #6d28d9;
}
/* Last row of a grid needs no bottom border */
.grid-item:nth-last-child(-n+3) {
border-bottom: none;
}
5. The of S syntax: understanding filtered counting
The extended syntax nth-child(An+B of S) adds an extra filter to the formula: instead of counting all siblings, only those additionally matching the selector S are counted. li:nth-child(2n of .featured) counts exclusively the elements carrying the class featured and selects every second one of those, regardless of which absolute positions those elements occupy within the full list.
This syntax solves a problem that classic nth-child alone could not solve: position counting within a filtered subset rather than the complete sibling list. Before the of S syntax, one either had to introduce an additional wrapper element per category or fall back to JavaScript to, for example, mark every second highlighted product in a mixed product list. Browser support for of S is now present across all current evergreen browsers.
/* Count only elements matching .featured, then select every 2nd of those */
li:nth-child(2n of .featured) {
outline: 2px solid gold;
}
/* First 2 featured items in a mixed list, regardless of absolute position */
li:nth-child(-n+2 of .featured) {
transform: scale(1.05);
}
6. Recipe: grid layouts with row break logic
In CSS grid layouts with a fixed column count, nth-child is the standard tool to specifically style elements at the start or end of a row, for example to insert horizontal separator lines only between rows, not between columns. In a grid with four columns, .item:nth-child(4n+1) always selects the first element of every row, which is useful for left aligned extra styling or removing a left margin at the start of the row.
A related pattern concerns variable border lines: .item:not(:nth-child(4n)) adds a right border to every element except the last one in every row, so no superfluous line appears at the row's end. This technique is far more robust than manually setting classes in the markup, because it stays automatically in sync with the actual position in the DOM, even when the number of elements changes due to dynamically loaded content.
7. nth-child vs. nth-of-type: the decisive difference
nth-child counts all sibling elements regardless of tag name, while nth-of-type only counts siblings of the same tag name. The difference becomes relevant as soon as different element types are mixed inside the same container: in a container with alternating h3 and p elements, p:nth-child(2) counts the absolute position among all children, while p:nth-of-type(2) counts the second p element regardless of how many h3 elements sit in between.
This difference is a common source of bugs: developers often expect the behavior of nth-of-type but accidentally use nth-child, producing wrong matches with mixed markup. The rule of thumb: use nth-of-type as soon as several different element types occur as siblings and only a specific type should be counted, use nth-child as soon as the absolute sibling position actually matters, for example in a homogeneous list consisting purely of li elements.
8. Recipe: pagination groups and separator lines
A practical recipe for nth-child is visually grouping pagination buttons or long lists into blocks, for example inserting a visual separator after every five entries. li:nth-child(5n)::after { content: ""; display: block; margin-block: 0.5rem; border-bottom: 1px solid #e2e8f0; } automatically inserts a separator line after every fifth element, without having to replicate the grouping in the markup.
For tables with many rows, the same principle can be used to draw a thicker separator line every five or ten rows, improving the readability of long datasets, a pattern familiar from printed spreadsheets. tr:nth-child(10n) marks every tenth row with a reinforced bottom border, which noticeably improves orientation especially in tables with hundreds of rows, entirely without additional grouping logic on the backend.
9. nth-child formulas compared directly
The following table shows common requirements and the matching nth-child formula for them, as a quick reference for daily project work.
| Requirement | Formula | Explanation |
|---|---|---|
| First three elements | :nth-child(-n+3) |
Negative A stops at position three |
| Last three elements | :nth-last-child(-n+3) |
Counting from the end of the list |
| Range 4 through 8 | :nth-child(n+4):nth-child(-n+8) |
Two formulas as an intersection |
| Every third filtered element | :nth-child(3n of .tag) |
Counting only within the subset |
| All except first and last | :not(:first-child):not(:last-child) |
Double negation instead of a formula |
Combining several nth-child selectors as an intersection, as shown in the range example, is the key to more complex patterns. Instead of searching for a single, complicated formula, almost every requirement can be assembled from simple, easily understood sub formulas.
Mironsoft
Grid layouts, lists and Hyvä themes without unnecessary markup
Position based styling without extra classes in the markup?
We replace manually maintained position classes with precise nth-child formulas, for leaner markup and grid layouts that automatically grow with dynamic content.
CSS Audit
Identifying manual position classes and replacing them with nth-child
Grid Refactoring
Making row break logic and separator lines robust against dynamic content
Hyvä Integration
Cleanly integrating nth-child patterns into Tailwind and Alpine components
10. Summary
nth-child is far more than odd and even: the underlying An+B formula allows arbitrary step sizes, offsets and, through negative values, range selection as well. nth-last-child counts the same formula backward from the end of the list, enabling patterns that work independently of the total element count. The of S syntax adds filtered counting, allowing position selection within a subset rather than the complete sibling list.
Anyone mastering these formulas can express grid row breaks, pagination groups, range selection and position filtering directly in the selector, without maintaining extra classes in the markup. The difference between nth-child and nth-of-type remains the most common source of errors and should always be checked deliberately with mixed markup before a formula is considered finished.
nth-child Formulas: The Key Points at a Glance
An+B Basic Formula
A determines the step size, B the offset. Negative A produces range selection starting at position one.
nth-last-child
Counts from the end of the list, ideal for patterns that should work independently of the total count.
of S Syntax
Counts only elements additionally matching a filter, for position selection within a subset.
nth-child vs. nth-of-type
nth-child counts all siblings, nth-of-type only those with the same tag name. Common confusion with mixed markup.