Road rescue module completed
35 h of training · final assessment 17/20
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.
Updated
This effect is free — the Transitions category contains 52 effects total, including 9 free. Effect.Labs has 840 vanilla effects. Explore the category →
35 h of training · final assessment 17/20
Which day suits you?
Week of 3 November · Lamballe centre
12 free slots, from earliest to latest
The jury signed the last sheets on Sunday at 6 pm.
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.
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.divs: they say nothing. If the grid carries information (badges, time slots), write it in the elements themselves, not in a decoration.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.
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.
Copy the three blocks into your page. No dependencies.
<div class="md-stagger-stage">
<div class="md-stagger"></div>
</div>
.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;
}
}
(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);
});
})();
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
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. |
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 }.
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.
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.