CSS Reflection Effects

CSS reflections with transforms and masking.

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.

tsx
<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 original
  • w-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

css
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)

css
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

tsx
// 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)

css
transform-gpu

Forces hardware acceleration. Smooth animations even if you have 20 cards on screen.

The Full Component

tsx
<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

tsx
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.

Command Palette

Search for a command to run...