CSS Reflections That Don't Look Like Trash
Most CSS reflections look kinda like someone discovered Photoshop filters in 2005.
I built this music player interface with reflections that actually look decent. The kind you'd see on a real glass surface, not some weird digital artifact.
Look at that hover effect. Smooth as butter.
Why Bother With Reflections?
Because flat design is kinda boring and we're not making apps for 2013 anymore.
Reflections add depth without being obnoxious about it. When done right, they make your interface feel premium. When done wrong... well, you've seen those websites.
What I Built Instead
Duplicate the image, flip it, mask it, profit.
<img
src={song.thumbnail}
alt={`${song.title} shadow`}
className="pointer-events-none absolute top-full left-0 w-full h-full object-cover rounded-lg opacity-100 transform scale-y-[-1] transition-all group-hover:translate-y-2 group-hover:opacity-0 duration-100 transform-gpu"
style={{
zIndex,
maskImage: "linear-gradient(to top, rgba(0,0,0,0.3) 10%, transparent 20%)",
WebkitMaskImage: "linear-gradient(to top, rgba(0,0,0,0.3) 10%, transparent 20%)",
}}
/>
That's the whole trick. Clone the image, flip it upside down, fade it out with a mask.
Breaking It Down
Positioning (The Boring Part)
absolute top-full left-0
: Sticks the reflection right below the originalw-full h-full
: Same size as the original (obviously)pointer-events-none
: So you can't accidentally click the reflection like an idiot
The Mirror Trick
transform: scale-y-[-1]
Flips the image upside down. Way more performant than CSS filters and actually works.
The Gradient Mask (Where the Magic Happens)
maskImage: linear-gradient(to top, rgba(0,0,0,0.3) 10%, transparent 20%)
This is what makes it look real:
- Starts at 30% opacity from the bottom
- Fades to transparent by 20%
- Creates that natural reflection fade
WebKit prefix included because Safari is still Safari.
The Hover Magic
// Original image moves up on hover
className="group-hover:-translate-y-3"
// Reflection moves down and fades out
className="group-hover:translate-y-2 group-hover:opacity-0"
When you hover:
- Original image lifts up 12px
- Reflection drops down 8px and fades out
- Looks like the card is actually lifting off the surface
It's simple but it sells the effect.
GPU Acceleration (Because We're Not Animals)
transform-gpu
Forces hardware acceleration. Smooth animations even if you have 20 cards on screen.
The Full Component
<div className="relative w-full h-full">
{/* Original Image */}
<div
className="w-full h-full cursor-pointer group"
onMouseEnter={() => setActiveIndex(index)}
onMouseLeave={() => setActiveIndex(null)}
>
<img
src={song.thumbnail}
alt={song.title}
className="w-full h-full object-cover rounded-lg shadow-lg transform transition-transform group-hover:-translate-y-3 transform-gpu duration-100"
/>
</div>
{/* Background Separator */}
<div
className="pointer-events-none absolute top-full left-0 w-full h-full rounded-lg transition-all duration-50 not-group-hover:bg-background group-hover:opacity-0"
style={{ opacity: 1, zIndex }}
/>
{/* Reflection */}
<img
src={song.thumbnail}
alt={`${song.title} shadow`}
className="pointer-events-none absolute top-full left-0 w-full h-full object-cover rounded-lg opacity-100 transform scale-y-[-1] transition-all group-hover:translate-y-2 group-hover:opacity-0 duration-100 transform-gpu"
style={{
zIndex,
maskImage: "linear-gradient(to top, rgba(0,0,0,0.3) 10%, transparent 20%)",
WebkitMaskImage: "linear-gradient(to top, rgba(0,0,0,0.3) 10%, transparent 20%)",
}}
/>
</div>
Three layers: original, separator, reflection. The separator handles the background color between them.
The Staggered Layout
const zIndex = filteredSongs.length - index;
const leftOffset = index * (cardWidth - overlapAmount);
Calculated z-index and positioning for that card stack effect. Math is annoying but it works.
Browser Support
CSS mask works everywhere that matters:
- Chrome/Safari: Obviously
- Firefox: Works with WebKit prefix
- Edge: Yep
- Internet Explorer: Who cares
Gracefully degrades to no reflection on ancient browsers.
Wrap It Up
Reflections that don't suck:
- GPU accelerated
- Actually look realistic
- Smooth hover effects
- Zero jank
The trick is the gradient mask and proper timing. Get those right and you've got reflections that look premium instead of cheesy.
Steal this code. Your users will think you're fancy.