The CSS :has()
selector has unlocked a bunch of new possibilities to select elements using CSS. A while ago I detailed how you can select elements based on the number of children they have using CSS :has()
. Today we’ll look into using :has()
to select the first, second, …, last element from an “island of elements” that all have a certain class.
~
# The code
If you’re just here for the code, here it is. You can also see the code in action in the demo below.
/* :first-in-island-of-class(.special) – Selects the first element from an island of siblings have a certain class */
.special:not(.special + .special) {
…
}
/* :last-in-island-of-class(.special) – Selects the last element from an island of siblings have a certain class */
.special:not(:has(+ .special)) {
…
}
/* :single-in-island-of-class(.special) – Selects the element that forms a single-element island with that a certain class */
.special:not(.special + .special):not(:has(+ .special)) {
…
}
If you want to know how these work – along with even more selectors such as a :nth-in-island-of-class(.special)
selector – keep on reading 🙂
~
# Islands?
Before we jump in, let’s make sure we’re using the same lingo here. With “islands of elements” I mean groups of adjacent sibling elements that can be grouped together. For example, consider the following list of child elements:
- no class
.special
.special
.special
- no class
.special
.special
- no class
- no class
.special
- no class
Elements 2, 3,and 4 form an island, as they can be grouped together because they share the same class. Same with elements 6 and 7, they also form an island.
Entry 10 is also an island, even though it only consists of only individual element.
~
# The selectors
Using :has()
, we can detect these islands and style the first and last elements of each island.
Note that the selectors created below all have a rather 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 …
~
:first-in-island-of-class(.special)
This selects the first element from an island of siblings have a certain class. Multiple children in a parent can be selected, as there can be several “islands” of elements with that class.
.special:not(.special + .special) {
…
}
.special
selects all elements that are .special
. By appending :not(.special + .special)
we are excluding the .special
’s that are preceded by a .special
.
~
:last-in-island-of-class(.special)
This selects the last element from an island of siblings have a certain class. Multiple children in a parent can be selected, as there can be several “islands” of elements with that class.
.special:not(:has(+ .special)) {
…
}
It works by selecting any .special
that is not directly followed by another .special
.
~
:single-in-island-of-class(.special)
By combining :first-in-island-of-class(.special)
and :last-in-island-of-class(.special)
, it’s possible to detect islands with a class that consist of only 1 element.
.special:not(.special + .special):not(:has(+ .special)) {
…
}
This selects the .special
elements that are not preceded and not succeeded by any other .special
sibling.
~
# Demo
See the Pen by Bramus (@bramus) on CodePen.
~
# More Selectors
We are not limited to only selecting the first or last element in an island. It’s possible to select any element at position n
in an island …
:nth-in-island-of-class(.special)
By adding more + .special
clauses to the selection, it’s possible to select the :2nd-in-island-of-class
, :3rd-in-island-of-class
, etc.
/* :2nd-in-island-of-class(.special) */
.special:not(.special + .special) + .special {
…
}
/* :3rd-in-island-of-class(.special) */
.special:not(.special + .special) + .special + .special {
…
}
~
:nth-last-in-island-of-class(.special)
Same goes for a :nth-last-in-island-of-class
selector: add more + .special
clauses to the condition. Then tack an extra :has(+ .special)
onto the selector to jump to the correct element.
/* :2nd-last-child-in-island-of-class(.special) */
.special:not(:has(+ .special + .special)):has(+ .special) {
color: red;
}
/* :3rd-last-child-in-island-of-class(.special) */
.special:not(:has(+ .special + .special + .special)):has(+ .special + .special) {
color: green;
}
~
Selector Generator
Use the pen below to generate :nth-in-island-of-class(.special)
and :nth-last-in-island-of-class(.special)
selectors. Use the dropdowns to change the value for n
as well as the type of selection. The selector is not limited to classes, but is limited to a compound selector.
See the Pen 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:
Quantity Queries for “islands of elements” with the same class, thanks to CSS `:has()`
🏷 #css #selectors pic.twitter.com/P8HgL5w7li
— Bram.us (@bramusblog) December 13, 2022
~
🔥 Like what you see? Want to stay in the loop? Here's how:
Leave a comment