For as long as I've been a frontend developer, there's been one hard-and-fast rule: CSS only cascades down and forward.
We can style an element based on its parent, and we can style an element based on a previous sibling (.item:hover + .item), but we could never go backward. We couldn't style a parent based on its child, and we definitely couldn't style a previous sibling.
If you wanted to create a simple hover effect, like an image gallery where hovering one item makes the other items fade back, you had to reach for JavaScript. You'd write a script to add/remove classes like .is-hovered, .is-inactive-neighbor, etc. It was clunky and mixed our styling logic with our scripts.
That rule is finally broken. The :has() pseudo-class (often called the "parent selector") is here, and it's so much more powerful than just selecting a parent. It's a "conditional selector" that lets us style an element based on literally anything inside or after it.
Today, I want to show you a mind-blowing trick: how to use :has() to style the previous siblings of a hovered element.
The Goal: A Proximity-Based Gallery
Here's the effect we want:
- All items in a list are "inactive" (grayscale, small).
- When you hover an item, it becomes "active" (full color, scales up,
z-index: 5). - The items immediately next to it (before and after) become "semi-active" (less grayscale, scale up a bit,
z-index: 4). - The items two steps away become "barely-active" (a little grayscale, scale up a tiny bit,
z-index: 3).
Styling the next siblings is easy. We've had the "next sibling combinator" (+) for years.
/* This is the item we hover over */
.gallery__item:hover {
transform: scale(1.5) translateY(-14%);
filter: grayscale(0%);
z-index: 5;
}
/* This styles the *next* sibling */
.gallery__item:hover + * {
transform: scale(1.2) translateY(-10%);
filter: grayscale(50%);
z-index: 4;
}
/* This styles the sibling *two steps* after */
.gallery__item:hover + * + * {
transform: scale(1.12) translateY(-5%);
filter: grayscale(70%);
z-index: 3;
}
This is great, but it only creates a "one-way" effect. The items to the left of our hover remain dead. How do we fix this?
The Magic: :has() to Look Backwards
This is where the magic happens. We're going to use :has() to "look ahead" and style an element based on its neighbor's state.
Let's build the selector for the previous sibling.
.gallery__item:has(+ *:hover) {
transform: scale(1.2) translateY(-10%);
filter: grayscale(50%);
}
Let's translate that into plain English:
"Find an element with the class .gallery__item that :has() an immediately adjacent next sibling (+ *)... ...which is currently being hovered (:hover)."
The browser finds the item you're hovering over (.gallery__item:hover), looks at its previous sibling, and checks if that sibling matches the selector. It does! That previous sibling now gets the "semi-active" styles.
We can apply the exact same logic for the sibling two steps away:
.gallery__item:has(+ * + *:hover) {
transform: scale(1.1) translateY(-5%);
filter: grayscale(70%);
}
"Find a .gallery__item that :has() a sibling two steps away (+ * + *) which is currently being hovered (:hover)."
And just like that, we've achieved a fully interactive, proximity-based hover effect with zero JavaScript.
The Full Code Study
Here is the complete code so you can see it all working together.
The HTML
The HTML is simple and semantic. Just a <section> containing a list of <figure> elements.
<section class="gallery">
<figure class="gallery__item">
<img src="https...1.jpg" alt="" class="gallery__item__img" />
</figure>
<figure class="gallery__item">
<img src="https...2.jpg" alt="" class="gallery__item__img" />
</figure>
<figure class="gallery__item">
<img src="https...3.jpg" alt="" class="gallery__item__img" />
</figure>
<figure class="gallery__item">
<img src="https...4.jpg" alt="" class="gallery__item__img" />
</figure>
<figure class="gallery__item">
<img src="https...5.jpg" alt="" class="gallery__item__img" />
</figure>
<figure class="gallery__item">
<img src="https...6.jpg" alt="" class="gallery__item__img" />
</figure>
<figure class="gallery__item">
<img src="https...7.jpg" alt="" class="gallery__item__img" />
</figure>
</section>
The CSS
Here, I've un-nested your SCSS/SASS into valid, flat CSS for clarity.
*,
*::before,
*::after {
margin: 0;
box-sizing: border-box;
}
body {
display: grid;
place-content: center;
min-height: 100svh;
background-color: hsl(20, 30%, 60%);
}
.gallery {
display: flex;
gap: 0.4rem;
padding: 1rem;
}
.gallery__item {
/* This is a great flex trick!
flex: [grow] [shrink] [basis];
"1 0 max(100px, 12%)" means:
- Grow if there's space.
- Don't shrink.
- Your base size is 12% of the container,
but never get smaller than 100px.
*/
flex: 1 0 max(100px, 12%);
aspect-ratio: 9/14;
box-shadow: 0 10px 10px -5px rgb(0 0 0 / 30%);
/* Start everything as grayscale */
filter: grayscale(100%);
transition:
transform 0.25s ease-in,
filter 0.25s ease-in;
}
.gallery__item img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
/* --- The :has() Magic (Previous Siblings) --- */
/* Selects the item 2 steps BEFORE the hover */
.gallery__item:has(+ * + *:hover) {
transform: scale(1.1) translateY(-5%);
filter: grayscale(70%);
z-index: 3;
}
/* Selects the item 1 step BEFORE the hover */
.gallery__item:has(+ *:hover) {
transform: scale(1.2) translateY(-10%);
filter: grayscale(50%);
z-index: 4;
}
/* --- The Main Hover (Current Item) --- */
.gallery__item:hover {
transform: scale(1.5) translateY(-14%);
filter: grayscale(0%);
z-index: 5; /* Highest z-index */
}
/* --- The Old-School Trick (Next Siblings) --- */
/* Selects the item 1 step AFTER the hover */
.gallery__item:hover + * {
transform: scale(1.2) translateY(-10%);
filter: grayscale(50%);
z-index: 4;
}
/* Selects the item 2 steps AFTER the hover */
.gallery__item:hover + * + * {
transform: scale(1.12) translateY(-5%);
filter: grayscale(70%);
z-index: 3;
}
New Possibilities
This single selector opens up a new world of design. We're not just limited to image galleries. Think about:
- Pricing Tables: Hover one plan and have the others react.
- Interactive Timelines: Hover a timeline event and have the previous and next events fade in or move.
- Navigation Menus: Hover a menu item and have its siblings move out of the way.
It's a huge step forward for CSS, and it lets us go back to building declarative, clean, and performant animations right where they belong: in the stylesheet.
