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

Motion Spring Easing Stagger Gesture Physics Bezier Timing

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 itemsSuggested intervalTotal wait time
3–560–80ms180–320ms
6–1040–60ms200–500ms
11–2020–40ms200–760ms
20+10–20msKeep 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.