Why Directly Animating box-shadow Is Expensive
A common pattern is growing a card's shadow on hover to suggest it's lifting off the page. The straightforward way to do this — animating box-shadow directly — forces the browser to repaint the shadow on every single frame of the animation, since the shadow's shape genuinely changes each frame. On a single element this is barely noticeable; on many elements animating simultaneously (a grid of hover cards, for instance), the repaint cost adds up and can visibly hurt smoothness.
The Better Technique: Animate Opacity Instead
Rather than changing the shadow's actual values, place a second, larger shadow on a pseudo-element positioned behind the main element, and animate that pseudo-element's opacity instead of the shadow itself:
.card {
position: relative;
box-shadow: 0 1px 2px rgba(0,0,0,0.15);
}
.card::after {
content: "";
position: absolute;
inset: 0;
box-shadow: 0 16px 32px rgba(0,0,0,0.25);
opacity: 0;
transition: opacity 0.2s ease;
}
.card:hover::after {
opacity: 1;
}
Opacity is one of the properties browsers can animate cheaply on the GPU, without triggering a full repaint each frame — the shadow itself never actually changes shape, only how visible it is, which is far less expensive to animate smoothly.
An Alternative: Faking Elevation With Scale
Another common technique combines a subtle transform: scale() on hover with the fixed shadow already in place, creating a sense of the element lifting slightly, without needing to animate the shadow at all — transforms are also GPU-composited and cheap to animate.
When Direct Animation Is Fine
For a single element, or an infrequent interaction (not something firing continuously across dozens of elements), directly animating box-shadow is often perfectly acceptable in practice — the performance concern scales with how many elements are animating simultaneously and how often.
Ready to generate the base shadow values for this technique?
Open Box Shadow Generator