CSS & layout

Rebuilding :nth-child() out of sibling-index(), if() and mod()

Rebuilding :nth-child() out of sibling-index(), if() and mod()

You wrote :nth-child(3n + 5) in a stylesheet. Now a design token says it should be every fourth item from the second. So you edit the selector. Then the selector in the media query. Then the one in the component variant.

The An + B numbers live inside the selector, and a selector can't read a custom property. That's the itch Temani Afif scratches in a new CSS-Tricks article. He rebuilds the :nth-child() patterns as value-level logic, using sibling-index(), the if() function with style() queries, and mod(). Once the numbers are custom properties, you can change them from anywhere the cascade reaches.

From selector to condition

Every example in the article uses the same markup: a .container with a run of plain div children. Nothing to see there. The interesting part is where the logic moves.

sibling-index() gives an element its position among its siblings as a number. Feed that number to a style() query inside if() and you get a per-element condition that sits in the declaration, not the selector. Here's the single-element case, the equivalent of :nth-child(5):

.container {
  --B: 5;
}

.container div {
  background: if(style(sibling-index() = --B): red; else: lightblue);
}

Every div matches the selector. Each one then works out its own background. The fifth one ends up red.

Swap = for >= and you have :nth-child(n + B), everything from position B onward. Swap it for <= and you have :nth-child(-n + B), the first B items. Same rule, one character changed. The article also builds a range version with two variables, an --X and a --Y, that marks every item between the two positions.

The modulo trick

Step patterns need arithmetic. :nth-child(An) means "every Ath element", which is a polite way of saying the index divides cleanly by A. That's what mod() checks:

.container div {
  background: if(style(mod(sibling-index(), var(--A)) = 0): red);
}

Set --A: 3 on the container and every third child goes red. Afif also guards the case where --A is negative, adding a second condition joined with and so the rule only fires when var(--A) > 0.

Then he goes for the full An + B. That one takes a couple of helper custom properties. The first is computed with calc() from the index, --B and --A. The second combines sign() and mod() to test whether that value is a whole number and not negative. There's also a branch for when --A is zero. I won't paste it here. Go read it with devtools open and watch the helper values change from item to item. It's the best part of the article.

Why this is worth your time

Here's my take. The real change is not "you can do :nth-child() without :nth-child()." It's that position-based styling becomes data.

A selector is fixed at authoring time. A custom property cascades. It inherits, it gets overridden by a more specific rule, and it can be set inline on one container. Afif names this as the main advantage: you adjust the selection through variables in real time without rewriting a selector. He also argues the conditional reads more naturally than An + B notation. I agree. sibling-index() >= --B tells you what it does. n + B makes you work it out.

The trade-off is real too. The selector version can apply a whole rule block at once. The if() version works one declaration at a time, so styling three properties means three if() calls, or one custom property you branch on. It's more verbose. You get flexibility in exchange.

Before you ship it

Afif is upfront here, and you should take it seriously. These are draft CSS features and browser support is limited. Treat this as a lab technique for now. Where :nth-child() does the job with fixed numbers, keep using :nth-child(). It works everywhere and needs no explaining in code review.

He ends with homework: build the :nth-last-child() version using sibling-count(). Try it. Counting from the end means working out how far each item sits from the last sibling, and that exercise will teach you more about these functions than another demo would.

Source: CSS-Tricks (css-tricks.com)

Related
CSS & layout

The class prefix selector: .btn-* is coming to CSS Selectors 5

The CSS Working Group has resolved to add a class prefix selector, written .prefix-*, to Selectors Level 5 — one rule for every class that begins with a shared hyphenated prefix, instead of a list or an attribute-selector hack.

August 20, 2026
Accessibility & semantics

Icons injected by CSS stay silent for screen readers, so icon-only buttons still need a name

Lea Verou proposes moving UI icons out of inline SVG and into CSS, as design tokens painted by masked pseudo-elements. The approach is quiet for assistive technology by default, and it gives icon-only controls a way to supply their own accessible name.

October 12, 2026
CSS & layout

A short :has() selector can outweigh the component it was never meant to touch

In Smashing Magazine, Gabriel Shoyombo walks through a card where .card:has(img) p matches a comment's paragraph and beats the comment's own rule on specificity, and shows how child combinators, :where() and consistent layering keep the selector in its lane.

November 3, 2026