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 hasan+b-1
siblings before it in the document tree, for any positive integer or zero value ofn
. For values ofa
andb
greater than zero, this effectively divides the element’s children into groups ofa
elements (the last group taking the remainder), and selecting thebth
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 amongAn+B
th elements from the list composed of their inclusive siblings that match the selector listS
, which is a<complex-real-selector-list>
. IfS
is omitted, it defaults to*|*
.
By passing a selector argument, you can select the N
th 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 ofB
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
.special
s 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.special
s, 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
.special
s using.special:not(:has(~ .special ~ .special ~ .special))
- Selecting all but the last 2
.special
s 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:
A `:nth-child(An+B [of S]?)` polyfill thanks to CSS `:has()`
🏷 #css #polyfill #selectors pic.twitter.com/LcS6NLbKdo
— Bram.us (@bramusblog) December 14, 2022
~
🔥 Like what you see? Want to stay in the loop? Here's how:
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!