Sibling Scopes in CSS, thanks to :has() and ~

Leverage CSS :has() to select all siblings between two element boundaries.

~

# Creating a Sibling Scope

Say you have markup like this:

<ul>
  <li>outside</li>
  <li class="from">from</li>
  <li>in-between</li>
  <li>in-between</li>
  <li>in-between</li>
  <li>in-between</li>
  <li class="to">to</li>
  <li>outside</li>
</ul>

If you want to select all elements between that .from and .to element, you can do so using this selector powered by the almighty :has() selector:

.from ~ :has(~ .to) {
  outline: 1px solid red;
}

It works as follows:

  • .from ~ * will select all elements that are preceded by .from.
  • :has(~ .to) will select all elements that are followed by a .to.
  • By combining both, you can clamp the selection and create a scope between the .from and .to siblings.

If you want to included the boundaries, create a selector list:

.from,
.from ~ :has(~ .to),
.to {
	outline: 1px solid red;
}

~

# Technical Demo

See the Pen Sibling Scopes by Bramus (@bramus) on CodePen.

~

# Limitations

As shown in the technical demo, the .from ~ :has(~ .to) is greedy. If you have two adjacent sets of boundaries that are also siblings from each other, the selector will select everything in between the first .from up to the last .to.

<ul>
  <li>outside</li>
  <li class="from">from</li>
  <li>in-between</li>
  <li>in-between</li>
  <li class="to">to</li>
  <li class="from">from</li>
  <li>in-between</li>
  <li>in-between</li>
  <li class="to">to</li>
  <li>outside</li>
</ul>

Depending on the use-case, this might or might not be considered a limitation.

Furthermore, this selector is a “heavy” one to match, so it could cause performance issues when used abundantly or on a large DOM. Use with caution.

~

# 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

UPDATE: As reader Paweł Grzybek points out, you can use the following selector in browsers that don’t support :has()

.from ~ :not(.to):not(.to ~ *) {
    outline: 1px solid red;
}

The selector doesn’t play nice with the last example of the technical demo – it only targets the first group – but depending on the use-case that might be acceptable.

~

# Practical Application

My colleague Jhey built a date picker that highlights the days in between your preferred start and end day.

Because they need to select elements across <tr> elements, the code is pretty wild and a bit more difficult to grasp. Basically it targets all cells between the one that has a :checked input and the cell your are currently hovering.

🤔 This would be much easier to style if a list + CSS grid were used to build the calendar. But that probably has some accessibility implications so yeah, no, … It Depends™, right?

~

# 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

4 Comments

  1. /* Only first group */
    .upper ~ :has(~ .lower):not(.lower ~ *, .lower) {
    outline: 1px solid green;
    }

    /* Only last group */
    .upper:not(:has(~ .upper)) ~ :has(~ .lower) {
    outline: 1px solid red;
    }

    /* Only first and last group */
    .upper ~ :has(~ .lower):not(.lower ~ :has(~ .upper), .lower, .upper) {
    background-color: lightgray;
    }

    1. Nice. It would need extra adjustments to play nice with more than 2 groups though, so it doesn’t really scale.

      Personally I consider this greediness not a limitation, but could depend on the use case.

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.