CSS if() and sibling-index() Rebuild :nth-child() With Live Variables
A new CSS technique uses if() and sibling-index() to make :nth-child() arguments adjustable via variables, covering ranges, multiples and the full An + B form — bugs included.
- AuthorPriya Raman
- ChannelInteraction & Visual Craft
- Filed
- Read3 min

- The technique implements :nth-child(An + B) using CSS if(), style() queries and sibling-index()
- Ranges use chained conditions like --X <= sibling-index() <= --Y
- Negative --A values select elements wrongly unless an and (var(--A) > 0) guard is added
- --A = 0 makes mod() return NaN, requiring a sign()-based workaround
- The method extends to :nth-last-child() using sibling-count()
A new implementation of :nth-child() built on the CSS if() function and the sibling-index() value lets developers change selector arguments through custom properties instead of rewriting rules. Change --A or --B on the container, and the selection updates. No selector rewrite required.
That is the whole point. With the classic approach, div:nth-child(5n + 8) locks the 5 and the 8 into the stylesheet; the browser offers no way to feed those numbers from variables. The new technique, demonstrated in a step-by-step walkthrough, replaces the selector entirely with conditional styling:
.container { --A: 5; --B: 8; }
.container div {
background: if(style(/* condition */): red; else: lightblue);
}
How does the simplest case work?
For :nth-child(5), the condition compares the element's sibling index against --B:
.container div {
background: if(style(sibling-index() = --B): red);
}
This is valid, working CSS, thanks to the new if() syntax combined with style() queries. An else branch handles the inverse case, and the condition can be written four equivalent ways — sibling-index() = --B, sibling-index() = var(--B), var(--B) = sibling-index(), or --B = sibling-index(). The technique extends to any property, not just background. The cost is redundancy: each property repeats the condition.
What about ranges and multiples?
The selector :nth-child(n + 5) becomes a >= comparison; :nth-child(-n + 5) becomes <=. A range like :nth-child(n + X):nth-child(-n + Y) collapses into one chained condition, if(style(--X <= sibling-index() <= --Y): red) — arguably more readable than the original notation, which takes a few minutes to decipher if you don't use it daily.
Multiples need arithmetic. To implement :nth-child(3n), the condition checks mod(sibling-index(), var(--A)) = 0, selecting the 3rd, 6th, 9th elements and so on. Setting --A to 1 selects everything; setting it to 2 reproduces :nth-child(even).
Here the author flags a real bug. Negative values of --A should select nothing, since all indexes are positive, but mod() handles negative numbers and can still return 0 — so elements get wrongly selected. The fix adds a second condition with the and keyword: (mod(sibling-index(), var(--A)) = 0) and (var(--A) > 0). The if() syntax accepts as many conditions as needed, combined with and and or.
Can the generic An + B form fail?
Yes, and the walkthrough admits it. The full :nth-child(An + B) case expresses n = (index - B)/A and tests two things: that n is non-negative, via calc((sibling-index() - var(--B))/var(--A)) >= 0, and that index - B is a multiple of A, via mod(sibling-index() - var(--B), var(--A)) = 0.
Set --A to 0, though, and the selection goes wrong. mod() returns NaN because its second argument is zero, breaking the condition. The author's workaround defines intermediate variables — --n and a guard variable --g that must equal 1 for selection, using sign() — and then, in a deliberately playful move, leaves the full explanation as "homework" for readers, pointing to a companion post for the dissection.
Two honest caveats run through the piece. The author concedes you will probably never need a dynamic :nth-child() in production, and the resulting code is more verbose than the selector it replaces. The stated goals are adjusting arguments on the fly and, mostly, learning the new primitives: if(), style() queries, sibling-index() and mod().
The closing framing is worth noting: selectors are just sets of conditions that filter elements. A follow-up challenge extends the method to :nth-last-child(), with a hint that sibling-count() does the heavy lifting there. Interactive demos accompany each step, letting readers adjust --A and --B and watch the selection redraw.
Original: css-tip.com
More from Priya Raman
Show full bio
Market editor covering technology and product news in Product and interaction design, design systems at Interface Wire.
44 articles