Detect “islands of elements” with the same class, thanks to CSS :has()

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:

  1. no class
  2. .special
  3. .special
  4. .special
  5. no class
  6. .special
  7. .special
  8. no class
  9. no class
  10. .special
  11. 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:

~

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

1 Comment

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.