Selecting all siblings with the :has() function
Reaching an element's siblings in CSS used to be tough. :has() changes everything.
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”.
.outline + .red
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 .outline + .box + .box + .box + .box?
.outline + .box,
.outline + .box + .box + .box + .box {
background-color: #b94a38;
}
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 .outline 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 :has() for a spin. We can use it to style our intended box red, when the element after it has the .outline class.
.box:has( + .outline) {
background-color: #b94a38;
}
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.
.box:has( + .outline) {
background-color: #b94a38;
transition: .3s ease-in-out background-color;
}
/* Apply this when our box is hovered AND
has the outline classed box next to it */
.box:hover:has( + .outline) {
background-color: #ff5f1f;
}
Flipping the relationship
What if we flipped this, and have the red box go orange when the outlined box is hovered? With :has() it’s easy enough.
.box:has( + .outline) {
background-color: #b94a38;
transition: .3s ease-in-out background-color;
}
/* Apply this when our box is hovered AND
has the outline classed box next to it */
.box:has( + .outline:hover) {
background-color: #ff5f1f;
}
Selecting all siblings at once
See where this is going? Now, we’re going to move our selector from .box to the parent .box-row and use a * to select all the elements in the row, turning them orange.
Then we’ll change it, using :has() on the parent element to check if any of the .outline classes are hovered. Then we can apply a style using :not(), passing the :hover state through. Effectively saying that in this block, any boxes that aren’t hovered, should turn orange when our .outline class is hovered.
#has-example-row-5:has( .outline:hover ) > *:not(:hover) {
background-color: #ff5f1f;
}
Almost there. Let’s remove .outline from our statement, replacing it with another * selector. So now in this block, any boxes that aren’t hovered, should turn orange when our any of our elements are hovered..outline class
#has-example-row-6:has( *:hover ) > *:not(:hover) {
background-color: #ff5f1f;
}
Blurring the rest
We’re so close, but only because the rest is not specifically :has() related. We’re going to use placeskull.com to provide some images in our row blocks. Now we don’t benefit from the background color, let’s use filter: blur(4px) and apply that to the image at the end of our :has() statement. We’ll also need overflow: hidden on the images parent div to prevent the blur from going over the edges - this keeps it nice and contained.
#has-example-row-7 > * {
transition: .3s ease-in-out all;
}
#has-example-row-7:has( *:hover ) > *:not(:hover) img {
filter: blur(4px);
}
Lastly, we’ll put a transform: scale() 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.
.field:has(input:invalid) {
--field-border: var(--color-error);
--field-label: var(--color-error);
}
The .field 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.
.cards:has(input:checked) .card:not(:has(input:checked)) {
opacity: 0.35;
}
The .cards grid detects any checked input and targets every sibling that doesn’t have one. The selection state lives in the checkbox and :has() does the rest.
Tap any card to select it Plan selected
Card grid layout
Switch a grid from two columns to three when a third card is present - no container queries, no JS counting children.
.card-grid {
grid-template-columns: repeat(2, 1fr);
}
.card-grid:has(.card:nth-child(3)) {
grid-template-columns: repeat(3, 1fr);
}
The grid adapts to its own content, updating the layout according to the amount of cards present.