Stagger: Bringing a List to Life
Stagger is the simplest technique for making a group of elements animate with intent. Instead of everything appearing at once (chaos) or fully sequentially (too slow), stagger produces a rhythmic cascade.
Click Replay to watch the cascade again — 8 pills at a 60ms interval
The formula
/* CSS */
.item:nth-child(1) { animation-delay: 0ms; }
.item:nth-child(2) { animation-delay: 50ms; }
.item:nth-child(3) { animation-delay: 100ms; }
/* ... */
// JavaScript (dynamic)
items.forEach((el, i) => {
el.style.animationDelay = `${i * 50}ms`;
})
Two key variables: the base animation (what each element does) and the interval (the delay between elements).
Choosing the right interval
Interval too short (< 20ms): users don’t perceive the pattern — it looks simultaneous.
Right interval (30–80ms): a rhythmic cascade that the brain can parse as a sequence.
Interval too long (> 120ms): each element looks like its own separate animation — slow and disconnected.
| Number of items | Suggested interval | Total wait time |
|---|---|---|
| 3–5 | 60–80ms | 180–320ms |
| 6–10 | 40–60ms | 200–500ms |
| 11–20 | 20–40ms | 200–760ms |
| 20+ | 10–20ms | Keep total < 400ms |
Rule: the total time from the first item starting to the last item starting should not exceed 400ms. For a long list, reduce the interval rather than make the user wait.
Easing for stagger
Each element should use ease-out — fast in, slow to settle. This fits the “appearing out of thin air” pattern.
.item {
animation: slide-up 0.4s cubic-bezier(0, 0, 0.2, 1) both;
}
@keyframes slide-up {
from {
opacity: 0;
transform: translateY(16px);
}
}
Use both so the fill-mode applies the state both before and after the animation. No separate forwards needed.
For more suitable curves, see Cubic Bezier In Depth.
Stagger direction
Top-down (default): top items appear first. Fits lists read vertically.
Bottom-up: bottom items appear first. Use this when the important content is at the bottom (uncommon).
Center-out: the middle items appear first, spreading to both sides. Creates a strong focal point at the center.
Random: each item gets a random delay within the allowed range. Fits grids without a clear hierarchy (gallery, mosaic).
// Random stagger
items.forEach(el => {
el.style.animationDelay = `${Math.random() * 300}ms`;
})
Stagger with a scroll trigger
Stagger is often combined with the Intersection Observer to trigger when an element enters the viewport:
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (!entry.isIntersecting) return;
const items = entry.target.querySelectorAll('.item');
items.forEach((el, i) => {
el.style.animationDelay = `${i * 50}ms`;
el.classList.add('animate');
});
observer.unobserve(entry.target);
});
}, { threshold: 0.1 });
See more on scroll-trigger patterns in Scroll Reveal Patterns.
Spring for each item
Stagger delay is usually used with CSS animation (bezier-based). But with JS (Framer Motion), you can combine stagger delay with spring physics for each element:
// Framer Motion
container.variants = {
hidden: {},
visible: { transition: { staggerChildren: 0.05 } }
}
item.variants = {
hidden: { opacity: 0, y: 16 },
visible: { opacity: 1, y: 0, transition: { type: 'spring', stiffness: 300, damping: 24 } }
}
The result: cascade timing from stagger, physical feel from the spring within each element. See spring config in Spring Parameters.
Common mistakes
Stagger on re-render: if the list changes content (filter, sort), re-staggering everything is overwhelming. Stagger should run only on the first mount or on a scroll trigger.
Forgetting animation-fill-mode: both: the item flashes visible before the animation starts, because the delay hasn’t run yet but there’s no initial state.
Translating too far: translateY(40px) per item creates a “falling from on high” feeling. 8–16px is usually enough to give a directional cue without being disorienting.