Selecting all siblings with the :has() function
Dom Jay
April 26, 2023
import PlaceholderImage from '../../components/PlaceholderImage.astro';
import HasDemo1 from '../../components/demos/HasDemo1.astro';
import HasDemo2 from '../../components/demos/HasDemo2.astro';
import HasDemo3 from '../../components/demos/HasDemo3.astro';
import FYIBlock from "../../components/FYIBlock.astro";
Update - 23rd December, 2024
This is now supported in all evergreen browsers, so need for an enabled flag to deal with this support - hooray!
The problem with siblings
It's been notoriously difficult to select elements depending on their children (almost as hard as that one level on Crash Bandicoot when you have to ride a tiger down the Great Wall of China, or that level on Max Payne with the crying baby and the blood line path), or siblings that came before it. Super easy to say "let's make this box red when it is an element following on from an element with a class of outline".
{}
But how do we get the 3rd or 4th element away from that outlined box, and make it red? Keep on chaining selectors, like this ?
Yeah that did it too....but blimey it's awful isn't it. We also can't make the 1st box red if the third box our outlined box, based on the class. There's no example I can think of that would demonstrate how it doesn't work - feel free to enlighten me and I'll add it to this post.
Enter :has()
With this complication comes the perfect excuse to take for a spin. We can use it to style our intended box red, when the element after it has the class.
{}
Adding hover states
So given that we've got access to previous siblings, we've got the ability to add states to them. Make that red box turn orange on hover? No problemo.
{}
Flipping the relationship
What if we flipped this, and have the red box go orange when the outlined box is hovered? With it's easy enough.
{}
Selecting all siblings at once
See where this is going? Now, we're going to move our selector from to the parent and use a to select all the elements in the row, turning them orange.
{}
Then we'll change it, using on the parent element to check if any of the classes are hovered. Then we can apply a style using , passing the state through. Effectively saying that in this block, any boxes that aren't hovered, should turn orange when our class is hovered.
{}
Almost there. Let's remove from our statement, replacing it with another selector. So now in this block, any boxes that aren't hovered, should turn orange when ~~our class~~ any of our elements are hovered.
{}
Blurring the rest
We're so close, but only because the rest is not specifically related. We're going to use to provide some images in our row blocks. Now we don't benefit from the background color, let's use and apply that to the image at the end of our statement. We'll also need on the images parent div to prevent the blur from going over the edges - this keeps it nice and contained.
{}
Lastly, we'll put a on the image, and also add a background color to the images parent block to be able to cover the whitespace caused by the scale, and voila!
{}
Where :has() really earns its keep
Our demo above is a pretty straightforward example of :has() in action, but it really shows it's worth when it's replacing JavaScript. Below are three real-world patterns that benefit from using :has() instead.
Form state styling
The demo below shows how to highlight a label or fieldset when the input inside it is invalid.
The wrapper responds to how valid the input inside it is. One rule handles every field in the form.
Card selection with sibling dimming
When one card is selected, dim the others without toggling classes or writing JS.
The grid detects any checked input and targets every sibling that doesn't have one. The selection state lives in the checkbox and does the rest.
Card grid layout
Switch a grid from two columns to three when a third card is present - no container queries, no JS counting children.
The grid adapts to its own content, updating the layout according to the amount of cards present.
Discussion in the ATmosphere