A :nth-child(An+B [of S]?) polyfill thanks to CSS :has() and :not()

Following up on the previous posts where I shared how you can select the nth element from an “island of elements” by leveraging CSS :has(), I noticed it’s also possible to adjust it to do a broader selection amongst siblings, thereby polyfilling (part of) :nth-child(An+B [of S]?) and :nth-last-child(An+B [of S]?) – selectors that are currently only fully supported by WebKit/Safari.

~

# The code

If you’re just here for the code, here it is. You can see the code in action in the demo below. Use the selector generator below to generate your own selectors.

/* :nth-child(1 of .special) */
.special:not(.special ~ .special) {
    …
}

/* :nth-child(2 of .special) */
.special ~ .special:not(.special ~ .special ~ .special) {
    …
}

/* :nth-child(3 of .special) */
.special ~ .special ~ .special:not(.special ~ .special ~ .special ~ .special) {
    …
}

…
/* :nth-last-child(1 of .special) */
.special:not(:has(~ .special)) {
    …
}

/* :nth-last-child(2 of .special) */
.special:not(:has(~ .special ~ .special)):not(.special:not(:has(~ .special))) {
    …
}

/* :nth-last-child(3 of .special) */
.special:not(:has(~ .special ~ .special ~ .special)):not(.special:not(:has(~ .special ~ .special))) {
    …
}

If you want to know how these work keep on reading 🙂

Initially the :nth-child() polyfilled selectors above initially used :has(), but as reader Rupert Angermeier pointed out, those :has() clauses are not required there. For :nth-last-child() they still are. The code in this post has been updated to reflect this.

~

# :nth-child(An+B [of S]?)?

CSS Selectors level 3 introduced the :nth-child() pseudo-class selector

The :nth-child(an+b) pseudo-class notation represents an element that has an+b-1 siblings before it in the document tree, for any positive integer or zero value of n. For values of a and b greater than zero, this effectively divides the element’s children into groups of a elements (the last group taking the remainder), and selecting the bth element of each group.

This selector is mostly used to select the odd or even elements from a group of siblings, or to alternate the style of groups of elements.

tr:nth-child(2n+1) /* represents every odd row of an HTML table */
tr:nth-child(odd)  /* same */
tr:nth-child(2n+0) /* represents every even row of an HTML table */
tr:nth-child(even) /* same */

/* Alternate paragraph colours in CSS */
p:nth-child(4n+1) { color: navy; }
p:nth-child(4n+2) { color: green; }
p:nth-child(4n+3) { color: maroon; }
p:nth-child(4n+4) { color: purple; }

In CSS Selectors level 4, this selector got extended with an optional of S part.

The :nth-child(An+B [of S]? ) pseudo-class notation represents elements that are among An+Bth elements from the list composed of their inclusive siblings that match the selector list S, which is a <complex-real-selector-list>. If S is omitted, it defaults to *|*.

By passing a selector argument, you can select the Nth element that matches that selector. For example, you can select “the 2nd item with the class .bar like so:

:nth-child(2 of .bar) {
  color: red;
}

The selector basically first does a pre-selection of the children that match the given selector list, and only applies the An+B logic onto that subset of siblings. I have detailed this approach before in the CSS :nth-of-class selector.

At the time of writing only Safari supports this of S suffix. Chrome supports the of S syntax as of Chrome 111, which is currently still in preview. The Firefox tracking bug remains unchanged.

~

# The selectors

While playing around with the code to select the nth element from an “island of elements” by leveraging CSS :has() I noticed :has() can also be used to polyfill :nth-child(An+B [of S]?) and :nth-last-child(An+B [of S]?). We do this by using the general sibling combinator ~ instead of the adjacent sibling combinator +

ℹ️ To keep things easy, the generated selectors do not support the full An+B [of S]? logic, but come with the following limitations:

  • The full An+B logic is not supported but limited to only a individual value of B being set, e.g. :nth-child(3).
  • Selector lists for S are not supported, only 1 selector is accepted.

Both these limitations can be solved, but I’ll leave that up to you, dear reader, as an exercise.

⚠️ Note that the selectors created below all have a pretty high specificity. To keep it low, I suggest wrapping them inside a :where() which nullifies the specificity. If you want to bump up the specificity again, you could tack on a :not(). – E.g. :where(…):not(.foo) will have a specificity of (0,1,0).

🐌 Performance wise there’s also a thing or two to say about these selectors …

~

:nth-child(B of S)

The selector below will polyfill :nth-child(2 of .special):

.special ~ .special:not(.special ~ .special ~ .special) {
    …
}

It works by:

  • Selecting all .specials that are preceded by 1 .special using .special ~ .special, effectively selecting the 2nd, 3rd, 4th, … .special sibling.
  • Limiting that selection to exclude any .special that is preceded by 2 .specials, omitting the 3rd, 4th, … .special siblings from the initial selection.

By adding more ~ .special parts to the selectors, the 3rd, 4th, 5th, … .special sibling can be selected. See the selector generator below to create these.

~

:nth-last-child(B of S)

The selector below will polyfill :nth-last-child(3 of .special):

.special:not(:has(~ .special ~ .special ~ .special)):not(.special:not(:has(~ .special ~ .special))) {
    …
}

It works by:

  • Selecting the last 3 .specials using .special:not(:has(~ .special ~ .special ~ .special))
  • Selecting all but the last 2 .specials using .special:not(.special:not(:has(~ .special ~ .special)))
  • Taking the intersection from both selections.

By adding more ~ .special parts to the selectors, other siblings can be selected. See the selector generator below to create these.

~

# Demo

See the Pen Creating new Selectors with :has(): A :nth-child(n of S) polyfill by Bramus (@bramus) on CodePen.

~

# Selector Generator

The created selectors can be automatically generated, as it’s a matter of adding extra ~ .special parts to the selectors to move the needle. Use the dropdowns+input in the pen below to generate the selector you want.

See the Pen
Creating new Selectors with :has(): A :nth-child(n of S) polyfill
by Bramus (@bramus)
on CodePen.

~

# Browser Support

These selectors are supported by all browsers that have :has() support. At the time of writing this does not include Firefox.

👨‍🔬 Flipping on the experimental :has() support in Firefox doesn’t do the trick either. Its implementation is still experimental as it doesn’t support all types of selection yet. Relative Selector Parsing (i.e. a:has(~ b)) is one of those features that’s not supported yet – Tracking bug: #1774588

~

# Spread the word

To help spread the contents of this post, feel free to retweet its announcement tweet:

~

Published by Bramus!

Bramus is a frontend web developer from Belgium, working as a Chrome Developer Relations Engineer at Google. From the moment he discovered view-source at the age of 14 (way back in 1997), he fell in love with the web and has been tinkering with it ever since (more …)

Unless noted otherwise, the contents of this post are licensed under the Creative Commons Attribution 4.0 License and code samples are licensed under the MIT License

Join the Conversation

3 Comments

  1. Now we just need Firefox to implement :has(). There are a few bad bugs with the experimental flag version currently.

    Can’t wait to start using this, as soon as Firefox is ready!

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.