Bouncy Card Hover: A Tiny CSS Detail That Makes a Difference

Leroy · 1 Jun 2026 · 2 min read

The Bounce

Hover over a card on this site and it lifts up with a bounce. It scales slightly. It casts a deeper shadow. A tiny detail that makes the interface feel responsive.

The effect uses GSAP, which is already loaded on the site for the gooey menu. Since GSAP is available globally, adding card animations was a few lines of code:

document.querySelectorAll('.card-hover').forEach(function(card) {
  card.addEventListener('mouseenter', function() {
    gsap.to(card, {
      y: -8, scale: 1.02,
      boxShadow: '0 20px 40px rgba(0,0,0,0.2)',
      duration: 0.3,
      ease: 'power2.out',
      overwrite: 'auto'
    });
  });
  card.addEventListener('mouseleave', function() {
    gsap.to(card, {
      y: 0, scale: 1,
      boxShadow: 'none',
      duration: 0.2,
      ease: 'power2.out',
      overwrite: 'auto'
    });
  });
});

On mouseenter the card lifts by 8 pixels, scales up 2%, and gains a shadow. All in 300ms with GSAP's power2.out easing for smooth deceleration. On mouseleave it snaps back in 200ms.

The overwrite: auto flag stops rapid hover transitions from queuing up. Each new hover cancels the previous animation cleanly.

CSS Fallback

I keep a pure CSS version as the baseline. It only applies on devices with a real cursor:

.card-hover {
  will-change: transform, box-shadow;
}
@media (hover: hover) {
  .card-hover:hover {
    transform: translateY(-4px);
    box-shadow: 0 8px 25px rgba(0,0,0,0.08);
    transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1),
                box-shadow 0.3s ease;
  }
}

The CSS version serves two purposes. It is a fallback if GSAP fails to load. And it provides the will-change hint so the browser can optimize for the GSAP animation. The cubic-bezier 0.34, 1.56, 0.64, 1 gives the springy overshoot.

The Easing Curve

The easing is the same in CSS and GSAP. A standard ease or ease-out feels linear and robotic. cubic-bezier(0.34, 1.56, 0.64, 1) has a spring-like overshoot. The second value (1.56) is greater than 1, so the animation overshoots its final position before snapping back.

Easing Feels like
ease Default, flat
ease-out Smooth deceleration
cubic-bezier(0.34, 1.56, 0.64, 1) Springy bounce

Why GSAP Instead of Pure CSS?

CSS transitions interpolate between two states. GSAP gives me three things that are harder with CSS alone.

  1. Composing y and scale together. CSS transform: translateY(-8px) scale(1.02) works, but GSAP lets me animate each property independently with different easings if needed.
  2. Animating boxShadow as a number. CSS can transition box-shadow, but it is clunky. GSAP treats it as a numeric value and interpolates smoothly.
  3. overwrite: auto. Clean handling of rapid hover-ins and hover-outs without animation queue buildup.

Accessibility

The animation respects prefers-reduced-motion:

@media (prefers-reduced-motion: reduce) {
  .card-hover {
    transition: none;
  }
  .card-hover:hover {
    transform: none;
    box-shadow: none;
  }
}

If a user prefers reduced motion, the cards display their default state with no transform or shadow change.

The Result

Hover over the cards on the homepage or the blog listing. Each lifts with a springy bounce. A small touch that makes browsing feel more connected.

related posts

01 Jun
Bouncy Card Hover: A Tiny CSS Detail That Makes a Difference
How a single CSS class with a spring-like cubic-bezier adds a satisfying bounce to every card on this blog - and why small animations matter.
01 Jun
Adding a Gooey Menu Hover Effect with GSAP
How I added a viscous, liquid-like gooey hover effect to the navigation menu using GSAP and an SVG filter - no JavaScript framework required.
01 Jun
Tailwind CSS: Why I Use a Utility-First Framework for This Blog
How Tailwind CSS v4 powers this blog's design - from the Catppuccin-inspired dark mode to the responsive card layout - and why utility-first CSS wins for me.