ScrollReveal Staggered Feature Grid — reset: false Explained
ScrollReveal Staggered Feature Grid · Scroll · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="sfg-stage">
<div class="sfg-head">
<span class="sfg-tag">scrollreveal · reset: false</span>
<h2>Built for Scale</h2>
<p>Scroll down — each card reveals once, from the bottom, staggered across the grid.</p>
</div>
<div class="sfg-grid">
<div class="sfg-card"><div class="sfg-ico">⚡</div><h3>Instant Deploys</h3><p>Push to main, live in under 10 seconds.</p></div>
<div class="sfg-card"><div class="sfg-ico">🔐</div><h3>Zero-Trust Auth</h3><p>Every request verified, nothing implicitly trusted.</p></div>
<div class="sfg-card"><div class="sfg-ico">📦</div><h3>Edge Caching</h3><p>Static and dynamic content cached at 40+ PoPs.</p></div>
<div class="sfg-card"><div class="sfg-ico">🧮</div><h3>Usage Analytics</h3><p>Per-endpoint cost and latency, out of the box.</p></div>
<div class="sfg-card"><div class="sfg-ico">🔄</div><h3>Auto Rollback</h3><p>Bad deploys revert themselves within 30 seconds.</p></div>
<div class="sfg-card"><div class="sfg-ico">🧩</div><h3>Plugin API</h3><p>Extend the pipeline without forking the core.</p></div>
</div>
<p class="sfg-note">Scroll back up, then down again — the grid does <strong>not</strong> replay. That's intentional.</p>
</div>*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0c1016;color:#fff;min-height:150vh;padding:70px 24px}
.sfg-stage{max-width:760px;margin:0 auto;display:flex;flex-direction:column;align-items:center;gap:32px}
.sfg-head{text-align:center}
.sfg-tag{display:inline-block;font-size:11px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:#34d399;background:rgba(52,211,153,.12);border:1px solid rgba(52,211,153,.3);padding:5px 12px;border-radius:99px;margin-bottom:12px}
.sfg-head h2{font-size:clamp(26px,5vw,38px);font-weight:800;letter-spacing:-.02em}
.sfg-head p{font-size:13.5px;color:#8ba397;margin-top:7px}
.sfg-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;width:100%}
@media (max-width:680px){.sfg-grid{grid-template-columns:1fr}}
.sfg-card{background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.09);border-radius:16px;padding:22px;box-shadow:0 20px 50px -20px rgba(0,0,0,.7)}
.sfg-ico{font-size:24px;width:44px;height:44px;display:flex;align-items:center;justify-content:center;border-radius:12px;background:rgba(52,211,153,.14);margin-bottom:14px}
.sfg-card h3{font-size:15px;font-weight:700;margin-bottom:6px}
.sfg-card p{font-size:12.5px;color:#8ba397;line-height:1.5}
.sfg-note{font-size:12.5px;color:#5c7168;text-align:center;max-width:440px}
.sfg-note strong{color:#8ba397}// reset: false is a deliberate choice for a feature grid: once someone has
// seen the cards animate in, re-triggering the same fade every time they
// scroll past it again would feel repetitive rather than delightful. A hero
// section benefits from a replay; a mid-page feature grid usually doesn't.
var sr = ScrollReveal({ reset: false });
sr.reveal('.sfg-card', {
origin: 'bottom',
distance: '40px',
duration: 650,
easing: 'cubic-bezier(0.5, 0, 0, 1)',
interval: 110,
opacity: 0,
});Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.
What's included
Features
About this UI Snippet
ScrollReveal Staggered Feature Grid — Interval Stagger and reset: false, Explained

This is the simplest possible ScrollReveal setup — one selector, one .reveal() call — and it's worth studying precisely *because* it's simple: almost every visual decision in it is one configuration option, and getting those options right (rather than reaching for custom JS) is the actual skill.
The whole animation is six options
js
sr.reveal('.sfg-card', { origin: 'bottom', distance: '40px', duration: 650, easing: 'cubic-bezier(0.5, 0, 0, 1)', interval: 110, opacity: 0 });
origin: 'bottom' plus distance: '40px' means each card starts 40px below its resting position; opacity: 0 adds a fade on top of that translation (ScrollReveal combines them into one CSS transition automatically). duration and the cubic-bezier easing control the individual card's motion curve — this particular bezier, (0.5, 0, 0, 1), is a strong ease-in-out that starts and ends with near-zero velocity, giving the card a smooth, confident settle rather than a linear slide.
interval is why six cards read as one grid, not six separate pops
interval: 110 is the option doing the "staggered" work: it delays each subsequent match in DOM order by 110ms relative to the one before it. Because the six cards are laid out left-to-right, top-to-bottom in a 3-column CSS grid, the stagger visually reads as a wave sweeping across each row — row 1's three cards cascade left to right, then row 2 picks up right where row 1 left off. This is a side effect of DOM order matching visual order, not something the grid explicitly encodes; reordering the cards in HTML without reordering them visually (e.g. via CSS order) would make the stagger direction disconnect from what the eye sees.
reset: false, made explicit rather than left as a default
ScrollReveal's reset option defaults to false, so this snippet technically didn't need to set it — but it does so explicitly and comments why, because the choice is easy to get backwards. With reset: false, once a card has revealed, ScrollReveal detaches its observer for that element; scrolling back up and down again does nothing further to it. With reset: true (used deliberately in the companion hero text reset snippet), the element re-hides every time it exits the viewport and re-plays every time it re-enters — appropriate for a hero someone lands on repeatedly, actively wrong for a feature grid buried mid-page that a user might scroll past several times while reading; re-triggering the same six-card cascade on every pass would feel like the page is stuttering rather than delighting.
Reusing it
The entire visual identity of this pattern — direction, distance, stagger speed, easing — lives in one options object. Swap origin: 'bottom' for 'left' to change the sweep axis, or drop interval to 0 for cards that reveal in unison instead of cascading.
Step by step
How to Use
- 1Add the ScrollReveal CDNInclude the scrollreveal UMD build from the CDN panel.
- 2Paste HTML, CSS, and JSA 3-column, 6-card feature grid renders with all cards initially invisible.
- 3Scroll the grid into viewCards fade up from the bottom, cascading left to right, row by row, 110ms apart.
- 4Scroll away and backThe grid does not replay — reset: false means each card reveals exactly once.
- 5Tune the stagger feelRaise interval for a slower cascade, or set it to 0 for all cards revealing together.
- 6Add more cardsAppend another .sfg-card element — the single reveal() selector picks it up automatically.
Real-world uses
Common Use Cases
reveal() call.interval delaying each match by its position in the DOM.reset: false set deliberately so the reveal happens only once.Got questions?
Frequently Asked Questions
interval delays each matched element by its position in DOM order, and because the six cards are written in the HTML in the same left-to-right, top-to-bottom order they appear in the 3-column grid, the delay sequence lines up with the visual sweep. It is a side effect of DOM order matching visual order, not something the grid layout enforces.
With reset: false, ScrollReveal reveals each element once and then stops observing it — scrolling it out of view and back in does nothing further. With reset: true, it would re-hide the element every time it exits the viewport and animate it again every time it re-enters, which this snippet deliberately avoids so a feature grid a user scrolls past multiple times doesn't replay every pass.
It is the default, but setting it explicitly and commenting why documents the decision as intentional rather than incidental — someone reading the code later (including future you) shouldn't have to know ScrollReveal's default to understand that replay-on-rescroll was considered and rejected for this component.
Set interval to 0, or omit it entirely — with no interval, every matched card animates on the same timer relative to when it individually crosses the visibility threshold, so a grid fully inside the viewport at once would appear to reveal in unison.
A pure translate without opacity can look like the card was always there but just moving, especially with a short distance; a pure opacity fade with no motion can feel flat. Combining a modest 40px slide with a fade gives the reveal a sense of arrival without being a dramatic slide-in.
No — ScrollReveal animates transform (for the origin/distance translation) and opacity, both of which are compositor-only properties that never trigger layout or paint of surrounding elements, so the grid and page around it stay perfectly stable while cards animate in.