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:
Sibling Scopes in CSS, thanks to :has()
🏷 #css #selectors pic.twitter.com/qdtOuwgLzm
— Bram.us (@bramusblog) January 12, 2023
~
🔥 Like what you see? Want to stay in the loop? Here's how:
/* 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;
}
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.
Very nice! Can be useful on my CSS framework – Charts.CSS.