JM Family Design System

Staggering animations across too many elements

mediumMotionmotionlistsperformance-perception

Staggering 20+ list items 50ms apart creates a slow cascade the user has to wait through before they can interact with the page.

The wrong way

Applying a per-item delay to every card in a long list — the last card does not land until well after a second has passed.

tsxdo not copy
{items.map((item, i) => (
  <Card
    key={item.id}
    style={{
      animationDelay: `${i * 60}ms`,
      animation: 'fadeUp 200ms ease-out forwards',
    }}
  />
))}
// 20 items × 60ms = 1,200ms before the last card lands

Why

Stagger is a tool for guiding the eye, not a default decoration. With a handful of elements, a brief cascade subtly directs attention to the last one to settle. With twenty, the cascade becomes a one-second wait the user did not ask for — and on a list that re-renders frequently (search results, filtered tables), it becomes a one-second wait every time they touch a filter. The motion budget is being spent on something that doesn't pay the user back.

The right way

Cap the stagger at the first five-or-so items, or skip the stagger entirely on long lists and cross-fade the whole group instead.

tsx
const MAX_STAGGER = 6;
{items.map((item, i) => (
  <Card
    key={item.id}
    style={{
      animationDelay: i < MAX_STAGGER ? `${i * 60}ms` : '0ms',
      animation:
        'fadeUp var(--motion-duration-normal) var(--motion-easing-enter) forwards',
    }}
  />
))}

Related

Added in 1.2.0Last reviewed 2026-05-15