---
name: staggered-entrance-animation
description: Use when you want "Organized, polished, premium" - Animates groups of elements one after another.
---

# Staggered Entrance Animation

> **Category:** Motion / Interaction  -  **Personality:** Organized, polished, premium
>
> **Best use:** Feature grids, lists, galleries
>
> **Ratings:** Professional 5/5 - Casual 4/5 - Exotic 3/5 - Visual impact 4/5 - Difficulty 3/5 - Performance cost 2/5

## What it is

A staggered entrance reveals a **group** of elements one after another instead of all at once, so a feature grid, list or gallery *cascades* into view with a steady rhythm. Each item carries an index (a CSS custom property `--i`), and that index multiplies a fixed step to become the item's `animation-delay` — item 0 starts immediately, item 1 a beat later, and so on. It reads as deliberate and premium, which is why design systems, pricing grids and portfolios lean on it. Unlike a plain "smooth reveal" (which animates single elements), the whole point here is the **collective cadence** of many elements moving in sequence.

## Dependencies / CDN

**None - pure CSS** for the cascade itself (one `@keyframes` + an index-driven `animation-delay`). The trigger is **vanilla JS** (an `IntersectionObserver`, ~25 lines). The demo also loads display/body fonts - optional:

```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Archivo:wdth,wght@125,600;125,700;125,800&family=Familjen+Grotesk:wght@400;500;600;700&family=Martian+Mono:wght@400;500;600&display=swap" rel="stylesheet">
```

## HTML

Give every element in the group the `sea-item` class plus a sequential `--i` index. Keep the trigger control (Replay) *outside* the cascading container so it never hides itself.

```html
<div class="sea-stage" id="seaStage">
  <!-- control sits outside the cascade -->
  <button class="sea-replay" id="seaReplay" type="button">&#8635; Replay cascade</button>

  <div class="sea-panel" id="seaPanel">
    <header>
      <span class="sea-item" style="--i:0">Cadence - Design System</span>
      <h2  class="sea-item" style="--i:1">Ship interfaces that arrive in rhythm.</h2>
      <p   class="sea-item" style="--i:2">A composable kit of tokens, components and motion.</p>
    </header>

    <div class="sea-grid">
      <article class="sea-card sea-item" style="--i:0">... 01 Color Tokens ...</article>
      <article class="sea-card sea-item" style="--i:1">... 02 Type Scale ...</article>
      <article class="sea-card sea-item" style="--i:2">... 03 Motion Curves ...</article>
      <article class="sea-card sea-item" style="--i:3">... 04 Spacing Grid ...</article>
      <article class="sea-card sea-item" style="--i:4">... 05 Components ...</article>
      <article class="sea-card sea-item" style="--i:5">... 06 Iconography ...</article>
    </div>
  </div>
</div>
```

## CSS

```css
/* 1) BASELINE - visible by default, so the content is fine with JS off */
.sea-item{ opacity:1; transform:none; }

/* 2) ARMED - JS adds .is-armed to the container, hiding items before the cascade */
.is-armed .sea-item{
  opacity:0;
  transform:translateY(26px) scale(.985);
  will-change:transform,opacity;
}

/* 3) THE CASCADE - one shared keyframe... */
@keyframes sea-rise{
  from{ opacity:0; transform:translateY(26px) scale(.985); }
  to  { opacity:1; transform:none; }
}
/* ...replayed per item, offset by its index. `both` holds the end state. */
.sea-run .sea-item{
  animation:sea-rise .64s cubic-bezier(.22,1,.36,1) both;
  animation-delay:calc(var(--i,0) * .08s);   /* <-- the whole trick */
}
/* optional: let the grid start AFTER the header, as one continuous flow */
.sea-run .sea-grid .sea-card{ animation-delay:calc(.24s + var(--i,0) * .085s); }

/* 4) REDUCED MOTION - reveal the whole group at once, no cascade */
@media (prefers-reduced-motion: reduce){
  .is-armed .sea-item{ opacity:1; transform:none; }
  .sea-run  .sea-item{ animation:none; opacity:1; transform:none; }
}
```

## JavaScript

Arm the items, then add `.sea-run` to fire the cascade. An `IntersectionObserver` plays it on scroll-in and re-arms it when the section fully leaves; a Replay button restarts it on demand.

```js
(function(){
  var stage  = document.getElementById('seaStage');
  var panel  = document.getElementById('seaPanel');
  var replay = document.getElementById('seaReplay');
  if(!stage || !panel) return;

  function play(){
    panel.classList.remove('sea-run');
    void panel.offsetWidth;            // reflow -> restart every item from frame 0
    panel.classList.add('sea-run');
  }

  if(replay) replay.addEventListener('click', play);

  // Respect the motion preference: leave everything visible, never cascade.
  var mq = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)');
  if(mq && mq.matches) return;

  panel.classList.add('is-armed');     // hide items, ready to cascade

  if('IntersectionObserver' in window){
    var live = false;
    new IntersectionObserver(function(entries){
      entries.forEach(function(en){
        if(en.isIntersecting && en.intersectionRatio >= 0.3){
          if(!live){ live = true; play(); }     // first scroll-in
        } else if(en.intersectionRatio === 0){
          live = false;                         // fully gone -> re-arm
          panel.classList.remove('sea-run');
        }
      });
    }, {threshold:[0, 0.3, 1]}).observe(stage);
  } else {
    play();                                     // no observer -> just play once
  }
})();
```

## How it works

- **`--i` is the sequence number.** A single rule, `animation-delay: calc(var(--i) * .08s)`, turns that integer into a per-item delay. One keyframe serves the whole group; the index is the only thing that differs.
- **One class flips the whole group.** Items start hidden via `.is-armed`; adding `.sea-run` to the parent makes every child run `sea-rise`. Because each child's `animation-delay` differs, they enter in order - the cascade.
- **`both` (fill mode)** keeps each item at its `from` frame during its delay and pins it at the `to` frame afterwards, so nothing flashes before its turn or snaps back when finished.
- **Replay = remove + reflow + re-add.** Removing `.sea-run`, forcing a reflow (`void panel.offsetWidth`), then re-adding it restarts the animation from frame 0 for every item simultaneously - the classic CSS-animation restart trick.
- **Scroll-in via `IntersectionObserver`** plays it when the section is 30% on screen and re-arms it once it's fully scrolled away, so scrolling back replays the cascade.

## Customization

- **Rhythm:** the `.08s` step is the dial. Smaller (`.05s`) = brisk and tight; larger (`.12s`) = slow and luxurious. Keep `step x itemCount` under ~1s so the tail doesn't drag.
- **Direction:** change the `from` transform - `translateX` for a slide-in, `scale(.9)` for a pop, `translateY(-26px)` to drop from above.
- **Easing/character:** swap the cubic-bezier. `cubic-bezier(.22,1,.36,1)` is a clean settle; `cubic-bezier(.34,1.56,.64,1)` adds a subtle overshoot for a playful feel.
- **Diagonal wave (grids):** instead of a flat index, set `--i` from row + column in your template engine, e.g. `--i: calc(var(--row) + var(--col))`, so a grid ripples corner-to-corner.
- **Auto-index without hand-writing:** `el.style.setProperty('--i', n)` in a `forEach`, or in modern CSS use `sibling-index()` where supported.

## Accessibility & performance

- **`prefers-reduced-motion`** is handled in both layers: the CSS shows every item at once, and the JS bails before arming/observing - so reduced-motion users get the final state instantly with **no per-item delay**.
- **Cheap by design:** the animation only touches `opacity` and `transform`, both GPU-compositable, so even a large grid stays at 60fps (hence the low performance rating). Never stagger `width`/`height`/`top`/`filter` - those trigger layout/paint per frame.
- **Content is safe without JS:** items default to `opacity:1`, so if the script never runs the group is simply visible (no blank section).
- **Don't trap focus or content:** opacity/transform keep elements in the accessibility tree and in the layout, so screen readers and keyboard order are unaffected during the reveal.

## Gotchas

- **No flash of unhidden content:** because the baseline is visible, hide the items by adding `.is-armed` as early as possible (a parser-blocking script at the end of `<body>`, as here) - otherwise eager-loading users may glimpse the content before it hides. Placing the section below the fold also avoids it.
- **Restart needs the reflow.** Re-adding `.sea-run` in the same tick without `void panel.offsetWidth` does nothing - the browser never sees the class leave, so the animation isn't re-triggered.
- **Keep the trigger out of the cascade.** A Replay button placed *inside* the staggered container will hide and re-animate itself on every replay; keep controls in a sibling toolbar.
- **`both` matters.** Drop the `both` fill mode and items snap visible during their delay (no backwards fill) and/or reset to hidden when the animation ends.
- **Long tails feel broken.** With many items a large step makes the last ones arrive seconds late; cap the delay (`min()`/clamp the index) or reduce the step for big grids.
