I saw a video going around recently with a title that makes me feel very old: “Crazy CSS Using By Master CSS”. The voice in it is also weird/fake/AI or something? Part of me finds it annoying and part of me finds it cool as I have a feeling English probably isn’t the creators native language and by making it English through the power of technology the video probably gets a broader audience. I guess it’s proof that if you make something super cool, the title doesn’t matter.
Look at me — I’ve digressed before I even started.
The demo they build in the video is fantastically cool. It’s a row of cards with a hover effect. The card being hovered has an effect where is appears to move toward you in 3D. Not only that, the two cards after the hovered card have their own unique effects, enhancing the 3D. But the ultimate touch is that the two cards before the hovered card also do, which has long been in “impossible” territory in CSS. No more!
Let’s cover that selector approach in CSS.
The element being hovered. Simple.
.card:hover {
}
Code language: CSS (css)
Now the next element after it:
.card:hover + .card {
}
Code language: CSS (css)
To be a bit more flexible and write a bit less code, we could write that like:
.card {
&:hover {
/* next card */
& + * {
}
}
}
Code language: CSS (css)
And we can keep going to the 2nd card after the hovered card:
.card {
&:hover {
/* next card */
& + * {
}
/* next card after that */
& + * + * {
}
}
}
Code language: CSS (css)
So now we have selected the hovered card itself and essentially the next two to the right. What about those cards on the left? The “previous siblings”, as it were?
Consider what we’ve already done. We’ve proven we can select the next card after a selector. So what if we could say “if the next card is hovered, select me?”. That’s what we can do with :has()
now in CSS.
.card {
&:hover {
/* Select previous sibling! */
:has(+ &) {
}
}
}
Code language: CSS (css)
If you find the nesting confusing, it’s just like this
:has(+ .card:hover) {
}
Code language: CSS (css)
Which says “any element where the next element is a hovered card.”
I’ll go one level deeper there, and combine it all:
.card {
&:hover {
/* next card */
& + * {
}
/* next card after that */
& + * + * {
}
/* previous card */
:has(+ &) {
}
/* previous card before that */
:has(+ * + &) {
}
}
}
Code language: CSS (css)
The selector part is the hard part here. Now that we’ve got that going, the styling could be anything. Keeping it a smidge simple at first, I’ll apply scale(1.3)
to the hovered element, scale(1.2)
to the next & previous sibling, then scale(1.1)
to the next & previous siblings after that. Plus a smidge of brightness filtering and we get:
(Just to prove how cool and smart I am, here’s me playing with this very idea two years ago.)
The video used some even cooler CSS to apply to the sibling cards, beginning with some 3D transform setup. The big idea was to use translateZ()
rather than scale()
to make the cards bigger (e.g. bring them “closer to the viewer”) which has a cool side effect of having an origin point that the cards appear to move away from. Coupled with a little rotateY()
on the siblings, the effect makes what looks like a mound of cards that follows the cursor. Bravo!
I didn’t see a live demo attached to the video in any way, so here’s my own re-creation (view big):
And a video in case you’re not on a device with a fine pointer.
Great article; thanks, Chris! It seems there’s a small typo in the code example that combines it all: the “previous card before that” bit uses :has(+ & + *), but I think it should use (+ * + &), which is indeed how it’s done in the Codepen examples.
Yes! Thank you. That was bending my brain as I was writing it, got it right in the Pen, and forgot to move over the update. Cheers Qil.
Gorgeous. :has() is really opening up doors to building UIs we’ve really only been able to make with JavaScript thus far. I’ve only recently started using it but I feel like this is going to be my most valued new CSS addition for 2024.