Home / Snippets / Animation /

Staggered entrance

Animate a list of elements into view sequentially using animation-delay and a --i custom property for each item's index.

🎯
Focus
Draws attention sequentially
🎨
Rhythm
Consistent timing feels polished
Performance
Compositor-only properties
Accessibility
Respects reduced-motion
🔧
Flexible
Any number of items via --i
🚀
Pure CSS
No JavaScript required
Widely Supported
animationno-js

Quick implementation

/* HTML: <div class="stagger-in" style="--i: 0"> */

.stagger-in {
  animation: stagger-in 0.5s ease-out both;
  animation-delay: calc(var(--i, 0) * 0.1s);
}

@keyframes stagger-in {
  from {
    transform: translateY(20px);
    opacity: 0;
  }
  to {
    transform: translateY(0);
    opacity: 1;
  }
}

@media (prefers-reduced-motion: reduce) {
  .stagger-in {
    animation: none;
    opacity: 1;
    transform: none;
  }
}

Prompt this to your LLM

Includes role, constraints, two framework variants, and edge cases to handle.

You are a senior frontend engineer implementing list entrance animations for a UI.

Goal: A staggered entrance animation that reveals list items or cards one after
another with incrementing delays — no JavaScript needed for the delay logic.

Technical constraints:
- Define a single @keyframes stagger-in rule: from { transform: translateY(20px);
  opacity: 0; } to { transform: translateY(0); opacity: 1; }.
- Apply animation: stagger-in 0.5s ease-out both; to a .stagger-in class.
- Set animation-delay: calc(var(--i, 0) * 0.1s); so each element controls its
  own delay via an inline CSS custom property: style="--i: 0", style="--i: 1", etc.
- Use oklch() for any colors — no hex or rgba values.
- Use CSS custom properties (var(--card), var(--text), etc.) for theming.
- Include @media (prefers-reduced-motion: reduce) that sets animation: none,
  opacity: 1, and transform: none on .stagger-in.

Framework variant (pick one):
A) Vanilla CSS — .stagger-in class applied in HTML with style="--i: N" on each item.
B) React component — accept an items array and render each with the .stagger-in
   class and an inline style={{ '--i': index }} prop.

Edge cases to handle:
- If the item count is dynamic, the --i values must be set server-side or via
  a template loop; document this so future maintainers know to update --i when
  reordering items.
- Very long lists (20+ items) accumulate large total delay; cap --i or reduce
  the multiplier (e.g. 0.05s) to keep total animation time under ~1 second.
- Ensure fill-mode "both" keeps items invisible before their delay fires and
  visible after their animation ends — do not omit it.

Return CSS only (or a React component if variant B is chosen).

Why staggered entrances create rhythm and hierarchy

When multiple elements appear on screen simultaneously, the eye has no cue about which to look at first. Staggered entrances solve this by turning a flat list into a sequence — each item arrives slightly after the last, guiding attention down the page in the natural reading order. The result feels intentional and polished rather than everything dumped onto the screen at once.

The technique also communicates hierarchy. Items that appear first are perceived as more important. In a feature list or card grid, staggering from top to bottom reinforces that the topmost item should be read first. Combined with a subtle upward slide (translateY(20px) to translateY(0)), the motion echoes the reading direction and feels physically grounded.

The calc(var(--i)) delay pattern

The conventional approach to staggered animation uses :nth-child selectors to hard-code each delay — .item:nth-child(2) { animation-delay: 0.1s; } and so on. This breaks the moment you add or remove items and becomes unmaintainable past a handful of elements.

The custom property pattern avoids this entirely. A single CSS rule handles every item: animation-delay: calc(var(--i, 0) * 0.1s). Each element declares its own index with an inline style="--i: N", and the browser computes the delay at paint time. Adding a new item means adding one attribute — no CSS changes needed. The fallback var(--i, 0) ensures that any item missing the attribute simply has no delay and still animates correctly.

The multiplier (here 0.1s) controls the feel of the stagger. Values around 0.08s0.12s feel snappy and connected. Larger values like 0.2s create a more dramatic, deliberate reveal. For long lists (10+ items), reduce the multiplier or cap the maximum index so the last item does not wait an uncomfortable amount of time.

Performance and accessibility

The stagger-in keyframe animates only transform and opacity — the two properties the browser compositor can handle entirely on the GPU, independent of layout and paint. Animating these properties costs nothing in terms of main-thread work and will remain smooth even when the page is doing heavy JavaScript work in the background. Avoid adding height, margin, or padding changes to entrance keyframes, as those trigger layout recalculation on every frame.

The prefers-reduced-motion: reduce query is not optional. Repeated sequential motion across many elements is more likely to cause discomfort than a single element animating, because the cumulative effect creates persistent movement across the viewport. Users who have enabled reduced-motion should see all items in their final, visible state immediately — no movement, no staggered pop-in.

The both fill mode is critical to correctness. Without it, items would flash at their final position before their delay fires, then animate from translateY(20px) — breaking the invisible-until-revealed effect. With animation-fill-mode: both (included in the shorthand), the from keyframe values are applied during the delay period, keeping the element invisible and shifted down until the animation actually begins.