Bouncy Card Hover: A Tiny CSS Detail That Makes a Difference
Leroy - Jun 1, 2026 - 2 min read
The Bounce
Hover over any card on this site - the blog listing, the service cards on the homepage, the related posts - and you'll notice it lifts up with a subtle bounce, scales slightly, and casts a deeper shadow. It's a tiny detail, but it makes the interface feel responsive and alive.
The effect uses GSAP, which is already loaded on the site for the gooey menu navigation. Since GSAP is available globally, adding card animations was just a few lines:
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 prominent shadow - all in 300ms with GSAP's power2.out easing for that smooth deceleration. On mouseleave it snaps back in 200ms.
The overwrite: 'auto' flag ensures rapid hover transitions don't queue up - each new hover cancels the previous animation cleanly.
CSS as Graceful Fallback
I still keep a pure CSS version as the baseline (applied via @media (hover: hover) so it only affects 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's a fallback if GSAP fails to load, and it provides the will-change hint so the browser optimizes for the GSAP animation. The cubic-bezier value 0.34, 1.56, 0.64, 1 gives that springy overshoot even in the CSS version.
The Secret: Cubic Bézier
The easing curve is the same for both CSS and GSAP. A standard ease or ease-out feels linear and robotic, but 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 can only interpolate between two states. GSAP gives me three things CSS can't easily do:
- Composing
yandscaletogether - CSStransform: translateY(-8px) scale(1.02)works, but GSAP lets me animate each property independently with different easings if needed - Animate
boxShadowas a number - CSS can transition box-shadow but it's clunky; GSAP treats it as a numeric value and interpolates smoothly overwrite: auto- clean handling of rapid hover-ins and hover-outs without animation queue buildup
Accessibility First
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 without any transform or shadow change.
The Result
Try it yourself - hover over the cards on the homepage or the blog listing. Each one lifts with a springy bounce. It's a tiny touch that makes browsing feel more connected and responsive.