TransitionsFree

Staggered Reveal

Twelve squares that come in one after another, 0.1 s apart, rising 20 px and growing from 0.8 to 1: the cascade starts when the block reaches the screen, then the grid stays put — 1.3 KB of JavaScript, your own elements accepted, placed at once when the user reduces motion.

CSSJSCascade

Updated

Hover or click the scene to interact

This effect is free — the Transitions category contains 52 effects total, including 9 free. Effect.Labs has 840 vanilla effects. Explore the category →

Usage examples

Firefighter Yanis Morel

Road rescue module completed

35 h of training · final assessment 17/20

① End-of-training badges — a platform for volunteer firefighters

WhenThe firefighter has just passed the road rescue module: they open their badges, and the twelve skills earned appear one by one, from scene marking to the radio report.
WhyThe cascade counts the skills one at a time: each badge gets its moment, instead of a block of icons you skim over. The order follows the call-out, from marking the scene to the radio report.
SettingsContainer as a 4-column grid (display: grid) instead of the flexible row; 12 badges of 121 × 48 px (30 px gold disc + label) in place of the squares; original delay and duration (0.1 s, 0.6 s), 1.7 s for the whole grid; showing the badge panel again replays the cascade.
Roadworthiness test · 45 min

Which day suits you?

Week of 3 November · Lamballe centre

② Free slots for a vehicle inspection — a car test center's website

WhenThe customer picks Thursday in the week: the twelve free slots from 8:00 to 17:15 arrive in time order, row by row.
WhyThe order of appearance follows the clock: the eye reads from 8:00 to 17:00 without searching, and the first slot, the most requested, is also the first one visible.
Settings4-column grid, read row by row; 12 pale green chips of 118 × 39 px, time in bold 15 px; no timing changed; a button goes back to the week, each new pass means a new cascade.
Vitré meeting · 8-9 Nov

Results awaiting validation

The jury signed the last sheets on Sunday at 6 pm.

③ Weekend medals — results page of a para athletics club

WhenThe jury has validated the meeting's results: the club publishes the page, and the eight medals arrive one after another, gold first, with the athlete's name and event.
WhyEach medal arrives on its own: each athlete gets their moment on screen, instead of a table where names blur together.
Settings8 cards of 117 × 99 px instead of 12 squares (1.3 s cascade); 4-column grid, 40 px medal in a gold, silver or bronze gradient; name in bold 11.5 px, event and class below; order gold → silver → bronze.

How it works

The .md-stagger-stage frame is 250 px tall, background #1a1a24. Inside, .md-stagger is a flexible row that wraps (flex-wrap: wrap, 8 px gap, 20 px padding). Each element gets the .md-stagger-item class: 40 × 40 px, 8 px corners, an indigo to violet gradient (#6366f1 → #a855f7). It starts invisible, 20 px lower and scaled to 0.8 (opacity: 0; transform: translateY(20px) scale(0.8)), and the mdStaggerIn animation brings it to its place, full size, in 0.6 s; forwards keeps it there. The whole offset fits in one line of the script: animation-delay is i × 0.1 s. The twelfth square starts at 1.1 s and stops at 1.7 s. Measured in Chromium 300 ms after the start: opacities 0.77 / 0.52 / 0.16 for the first three, 0 for the other nine.

The script walks through every .md-stagger on the page. If the container already has children, they are the ones that cascade in; if it is empty, it creates 12 demo squares. The play() function removes then re-adds each element's class, forcing a reflow in between (void item.offsetWidth): that is what restarts a CSS animation that has already played. An IntersectionObserver at a 0.3 threshold calls play() when 30% of the container becomes visible. Leaving the screen does nothing: the grid stays displayed. Coming back replays the cascade. Measured in Chromium, Firefox and WebKit: at 3.4 s then at 6.4 s, all twelve squares are still at opacity 1; after scrolling away and back, 150 ms after the return, opacities 0.35 / 0.04 / 0 — the cascade has started over. With var once = true, the observer disconnects after the first entry.

Why wait for the screen instead of playing on load? A cascade is there to make a grid read in order: played out of view, it is over before anyone looks at it. The container stays empty until it is reached, then the final state holds — content that vanished and came back every three seconds could not be read. The frame has its own class, prefixed like the rest of the effect: two effects from the library pasted on the same page do not overwrite each other. A variant in the category applies the same offset, on click, to a list leaving and coming back.

Accessibility

  • Reduced motion: under prefers-reduced-motion: reduce, the CSS removes the animation and puts each element in place (animation: none; opacity: 1; transform: none). Measured in Chromium 151, Firefox 153 and WebKit 26: all twelve elements are at opacity 1 from the first measurement, with no rise and no growth.
  • Screen readers: your own elements are in the page from load; the zero opacity at the start does not hide them from screen readers, which read them in source order. The 12 demo squares are empty divs: they say nothing. If the grid carries information (badges, time slots), write it in the elements themselves, not in a decoration.
  • Duration: the cascade lasts 1.7 s for 12 elements, once, then nothing moves. For a longer grid, keep that order of magnitude (see the FAQ): beyond 3 s, the last elements arrive when people have already started reading.
  • Keyboard: nothing in the effect is interactive. If your elements are links or buttons, they stay focusable during the cascade; the last one is visible 1.7 s after entering the screen at the latest.

Browser compatibility

CSS (@keyframes, animation-fill-mode: forwards, transform) + 1.3 KB of JavaScript (IntersectionObserver, NodeList.forEach), no dependency. Measured in Chromium 151, Firefox 153 and WebKit 26: same delays, same grid held after the cascade, same replay when coming back on screen, same immediate placement under reduced motion. IntersectionObserver exists since Chrome 51, Firefox 55 and Safari 12.1.

Chrome 51+✓ Full
Firefox 55+✓ Full
Safari 12.1+✓ Full
Edge 79+✓ Full
Mobile iOS 12.2+✓ Full
Android Chrome 51+✓ Full

Without IntersectionObserver, the script plays the cascade once, on load. Without JavaScript, the demo container stays empty; your own elements, if they already carry the md-stagger-item class in the HTML, all appear together on load (same rise, no offset). Without CSS animations they stay at opacity 0: in that case, do not put the class in the HTML and let the script add it.

The code

Copy the three blocks into your page. No dependencies.

HTML
<div class="md-stagger-stage">
  <div class="md-stagger"></div>
</div>
CSS
.md-stagger-stage,
.md-stagger-stage *,
.md-stagger-stage *::before,
.md-stagger-stage *::after {
  box-sizing: border-box
}

.md-stagger-stage {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 250px;
  overflow: hidden;
  position: relative;
  background: rgb(26, 26, 36);
  color: rgb(255, 255, 255);
  font-family: sans-serif;
}

.md-stagger {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 20px;
  flex-wrap: wrap;
}

.md-stagger-item {
  width: 40px;
  height: 40px;
  border-radius: 8px;
  background: linear-gradient(135deg, #6366f1, #a855f7);
  opacity: 0;
  transform: translateY(20px) scale(0.8);
  animation: mdStaggerIn 0.6s forwards;
}

@keyframes mdStaggerIn {
  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

/* Mouvement réduit : les éléments sont posés d'emblée, sans montée ni agrandissement */
@media (prefers-reduced-motion: reduce) {
  .md-stagger-item {
    animation: none;
    opacity: 1;
    transform: none;
  }
}
JavaScript (fx-0725)
(function() {
  // Staggered Reveal : chaque .md-stagger de la page entre en cascade quand il
  // arrive à l'écran, puis reste affiché. Il se rejoue s'il sort de l'écran et y revient.
  var once = false; // true : la cascade ne se joue qu'à la première entrée

  document.querySelectorAll('.md-stagger').forEach(function(container) {
    // Vos éléments s'il y en a dans le conteneur, sinon 12 carrés de démonstration
    var own = container.children.length > 0;

    function play() {
      if (!own) {
        container.innerHTML = '';
        for (var i = 0; i < 12; i++) container.appendChild(document.createElement('div'));
      }
      Array.prototype.forEach.call(container.children, function(item, i) {
        item.classList.remove('md-stagger-item');
        void item.offsetWidth; // relance l'animation
        item.classList.add('md-stagger-item');
        item.style.animationDelay = (i * 0.1) + 's';
      });
    }

    if (!('IntersectionObserver' in window)) {
      play();
      return;
    }
    var obs = new IntersectionObserver(function(entries) {
      if (!entries[0].isIntersecting) return;
      play();
      if (once) obs.disconnect();
    }, {
      threshold: 0.3
    });
    obs.observe(container);
  });
})();

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
Gap between two elements (JS — i * 0.1) 0.1 s Total duration is (number of elements − 1) × gap + 0.6 s. 0.05 s for a 20-element grid (1.55 s); 0.15 s for four large cards.
Duration and easing (CSS — animation: mdStaggerIn 0.6s forwards) 0.6 s, default easing (ease) 0.4 s makes the cascade snappier. A cubic-bezier(.2, .8, .2, 1) curve makes each element arrive fast and settle softly.
Starting point (CSS — transform: translateY(20px) scale(0.8)) 20 px lower, 80% of the size translateX(-16px) brings elements in from the left, useful for a list; scale(0.5) alone gives a blooming effect. Keep the @keyframes end state at zero.
Entry threshold (JS — threshold: 0.3) 30% of the container visible 0 starts at the first visible pixel, even if the cascade plays below the edge of the screen; 0.6 waits until the grid is almost entirely in view.
Only once (JS — var once = false) replays on every return to the screen true disconnects the observer after the first cascade: the grid then stays still, even after scrolling away and back.
Your elements (HTML — children of .md-stagger) none: 12 demo squares As soon as the container has children, they are the ones that cascade in, in source order. Give them width: auto; height: auto and their own background to replace the 40 px square.

FAQ

Why doesn't the grid replay in a loop?

Because a cascade goes with an arrival: once the grid is in, it must stay readable. The script only replays it when the container comes back on screen (after scrolling, or after being hidden). To restart it on demand, after a filter change for example, hide a parent of the container (hidden attribute) and show it again a moment later: the observer sees the grid come back and replays the cascade. That is what the three examples on this page do. The hidden attribute set on .md-stagger itself is not enough: its display: flex wins; in that case add .md-stagger[hidden] { display: none }.

How do I animate my own cards instead of the squares?

Put them inside .md-stagger: the script finds them and applies the class and the delay, in HTML order. Write the md-stagger-item class directly on each card: they are then hidden from load, with no flash before the cascade, and still show up if the script fails to load. Add width: auto; height: auto and your background to replace the 40 px gradient square; the layout (grid, list) is up to you.

What about a list of 40 elements?

At a 0.1 s gap, the 40th would arrive after 4.5 s: too long. Cap the delay — Math.min(i, 12) * 0.1 makes every element beyond the 12th start together, at 1.2 s, and be in place at 1.8 s — or reduce the gap to 0.03 s. On a long page, observe each block separately instead: one .md-stagger per section, each plays its cascade when it reaches the screen.