Box Shadow Animation Techniques: Smooth Elevation and Hover Effects
Why Animate Box Shadows?
Subtle shadow animations communicate interactivity without words. A button that lifts on hover, a card that gains depth on focus, or a dropdown that slides into view — these micro-interactions make interfaces feel responsive and polished.
But animating box-shadow the wrong way causes jank. This guide covers both the visual techniques and the performance patterns that keep animations smooth.
The Basic Transition
The simplest approach: transition between two box-shadow states.
.card {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
transition: box-shadow 0.3s ease;
}
.card:hover {
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
This works well for simple two-state animations. The browser interpolates each shadow value (offset, blur, spread, color) independently.
Transition Timing
| Duration | Use Case |
|---|---|
| 100–200ms | Small interactive elements (buttons, toggles) |
| 200–300ms | Cards, list items |
| 300–500ms | Modals, panels |
Faster than 100ms feels instant (no animation perceived). Slower than 500ms feels laggy.
Elevation on Hover
Simulating an element lifting off the page is one of the most common shadow animations. Combine a shadow change with a slight translateY shift for the most convincing effect:
.button {
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.06);
transform: translateY(0);
transition: box-shadow 0.2s ease, transform 0.2s ease;
}
.button:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.10), 0 8px 16px rgba(0, 0, 0, 0.08);
transform: translateY(-1px);
}
.button:active {
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
transform: translateY(0);
transition-duration: 0.05s;
}
The :active state reverses the elevation quickly, creating a "press-down" feel.
Animating Layered Shadows
When transitioning between layered shadow states, keep the same number of layers in both states. The browser interpolates corresponding layers positionally.
/* ✅ Same layer count — smooth interpolation */
.card {
box-shadow:
0 1px 2px rgba(0, 0, 0, 0.05),
0 4px 8px rgba(0, 0, 0, 0.05);
transition: box-shadow 0.3s ease;
}
.card:hover {
box-shadow:
0 2px 4px rgba(0, 0, 0, 0.08),
0 12px 24px rgba(0, 0, 0, 0.10);
}
/* ❌ Different layer count — first layer animates, second pops in */
.bad-card {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
transition: box-shadow 0.3s ease;
}
.bad-card:hover {
box-shadow:
0 2px 4px rgba(0, 0, 0, 0.08),
0 12px 24px rgba(0, 0, 0, 0.10);
}
Performance: The Repaint Problem
box-shadow animations trigger repaints because the shadow pixels must be recomputed each frame. On complex pages with many animated elements, this causes visible jank.
Solution 1: Pseudo-Element Opacity
Use a pseudo-element with the hover shadow, and animate only opacity — which the GPU composites without repaints.
.card {
position: relative;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
transition: transform 0.2s ease;
}
.card::after {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
opacity: 0;
transition: opacity 0.3s ease;
z-index: -1;
}
.card:hover {
transform: translateY(-2px);
}
.card:hover::after {
opacity: 1;
}
This technique is significantly smoother on low-powered devices.
Solution 2: CSS @starting-style (2026)
Modern browsers support @starting-style for entry animations, allowing you to animate from a shadow state without the "flash of no shadow" problem:
.card {
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
transition: box-shadow 0.3s ease;
}
@starting-style {
.card {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
}
}
Keyframe Animations
For cyclic or multi-step shadow animations, use @keyframes:
@keyframes pulse-shadow {
0%, 100% {
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.10);
}
50% {
box-shadow: 0 4px 16px rgba(59, 130, 246, 0.25);
}
}
.notification {
animation: pulse-shadow 2s ease-in-out infinite;
}
Use this sparingly — pulsing shadows draw attention but become distracting if overused.
Dark Mode Shadow Animations
In dark mode, animated shadows can expose color shifts as opacity changes. Use consistent shadow coloring within each mode:
.card {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
transition: box-shadow 0.3s ease;
}
@media (prefers-color-scheme: dark) {
.card {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.30);
}
}
Key Takeaways
- Combine
box-shadowtransitions withtranslateYfor convincing lift effects - Always match layer counts between shadow states for smooth interpolation
- Use pseudo-element opacity for performance-critical shadow animations
- Keep durations between 100–300ms for perceived responsiveness
- Animate
:activestate with fast transitions (50ms) for press feedback - Use keyframe animations for attention-drawing pulses — but sparingly
Try It Yourself
Build and animate box shadows visually with our CSS Box Shadow Generator. Adjust shadows interactively, preview hover transitions, and copy production-ready CSS.
Related Guides
- CSS Box Shadow Guide — complete syntax and parameter reference
- Layered Box Shadows for Depth — creating realistic elevation with multiple shadow layers