You can build a responsive, slide-and-fade staggered reveal in Webflow without writing a single line of JavaScript — just one attribute and a small block of CSS built around the transition-delay property. It’s the same idea we used for text animations, only now it adapts across breakpoints, survives any number of columns, and even works with the CMS.
The reason this needs a trick at all is that Webflow can’t see a row. It can trigger an animation when the whole grid scrolls into view, or when a single item does — but not when a full row crosses the viewport, which is exactly what a stagger needs. The native workarounds (splitting each row into its own grid) collapse the moment your columns reflow at a smaller breakpoint, and they can’t touch CMS items individually.
So instead of fighting Webflow, we let CSS do the part it’s great at. The interaction stays trivial; the intelligence lives in a few lines of CSS you can read, understand, and tweak to fit any layout.
How it works
The interaction is almost empty on purpose. Each grid item carries two CSS transitions — one for the move transform, one for opacity (in the reference project both run 800ms, with ease for opacity and ease-out cubic for the move). The Webflow scroll-into-view interaction then does the bare minimum: two actions, each with a duration of zero and no easing, sending the item back to translateY(0) and opacity: 100%. Set the trigger to affect the class, so building it once applies it to every sibling and keeps Webflow’s generated code lean. On its own this does nothing visible — the items are already in their final position — which is where the CSS comes in.
CSS sets the start and the rhythm. A block of custom CSS targets every item by its attribute ([fc-css-staggered="item"]) and gives them an initial state: shifted down (about 3rem on the Y axis) and opacity: 0. When an item scrolls in, its transition carries it from that start to the resting state. The stagger is nothing more than a different transition-delay per column: column one fires immediately, column two a beat later, and so on, so a row appears to ripple in.
:nth-child() is the whole engine. To give each column its own delay, the code uses one “building block” per column, shaped like [fc-css-staggered="item"]:nth-child(Nn - i). Two numbers do all the work: N, the coefficient before n, equals the number of columns at that breakpoint; i, the number after the minus, is the column index, running from N - 1 for the first column down to 0 for the last. So in a four-column grid, 4n - 3 catches the first column, 4n - 2 the second, and 4n the fourth.
Media queries make it responsive. Because a grid changes its column count per breakpoint, you wrap each set of building blocks in a @media (min-width: …) query — 480px (landscape), 768px (tablet), 992px (desktop), and so on. Each query overrides the one below it, so you only define what changes. That’s the key insight: adapting to a new layout is never a rewrite, just matching the coefficient to the column count and adding one building block per column.
How to use it
-
Build the grid and tag the items. Give every element inside the grid the attribute
fc-css-staggeredwith a value ofitem. Because the attribute lives on each element (not on a class), configure the first item, then duplicate it — the copies inherit the attribute automatically. -
Add the two transitions to the item. On the grid item, add a transition for the move transform and one for opacity (e.g.
800ms,ease-out cubicfor the move andeasefor opacity). These are what actually animate; the interaction only flips the end state. -
Build a minimal scroll-into-view interaction. Two actions — move to
translateY(0)and opacity to100%— both with a duration of0and no easing. Set an offset (around20%) and, under trigger settings, choose class so it applies to every item sharing that class. -
Paste the CSS. Drop it into the Global Styles component (site-wide), the page’s
<head>(that page only), or Site Settings → Custom Code → head (whole site). The block sets the initial state and a per-columntransition-delayinside a media query for each breakpoint. -
Tune it to your layout. For each breakpoint, keep one building block per column, set the coefficient before
nto the column count, set the index after the minus fromN - 1down to0, and give each column its delay. To add a new breakpoint (say a1440pxfive-column layout), copy a media query, change itsmin-width, bump the coefficient to five, add a fifth building block, and set its delays. -
Two grids, or the CMS. For a second grid, give its items
fc-css-staggered="item-2", duplicate the CSS, and swapitemforitem-2in the selectors — the two stagger independently. For a collection list, put the attribute on the collection item; nothing else changes, which is why this approach works where the native one can’t.
Here’s the shape of the CSS, reconstructed from the walk-through — the initial state, then one media query per breakpoint with a building block per column:
/* Initial state: every item starts shifted down and invisible */
[fc-css-staggered="item"] {
transform: translateY(3rem);
opacity: 0;
}
/* Portrait — 1 column, no stagger */
[fc-css-staggered="item"]:nth-child(1n - 0) { transition-delay: 0ms; }
/* Landscape — 2 columns, from 480px */
@media screen and (min-width: 480px) {
[fc-css-staggered="item"]:nth-child(2n - 1) { transition-delay: 0ms; } /* column 1 */
[fc-css-staggered="item"]:nth-child(2n - 0) { transition-delay: 100ms; } /* column 2 */
}
/* Tablet — 3 columns, from 768px */
@media screen and (min-width: 768px) {
[fc-css-staggered="item"]:nth-child(3n - 2) { transition-delay: 0ms; } /* column 1 */
[fc-css-staggered="item"]:nth-child(3n - 1) { transition-delay: 100ms; } /* column 2 */
[fc-css-staggered="item"]:nth-child(3n - 0) { transition-delay: 300ms; } /* column 3 */
}
/* Desktop — 4 columns, from 992px */
@media screen and (min-width: 992px) {
[fc-css-staggered="item"]:nth-child(4n - 3) { transition-delay: 0ms; } /* column 1 */
[fc-css-staggered="item"]:nth-child(4n - 2) { transition-delay: 100ms; } /* column 2 */
[fc-css-staggered="item"]:nth-child(4n - 1) { transition-delay: 300ms; } /* column 3 */
[fc-css-staggered="item"]:nth-child(4n - 0) { transition-delay: 600ms; } /* column 4 */
}
/* Add a 5-column breakpoint the same way:
@media screen and (min-width: 1440px) {
[fc-css-staggered="item"]:nth-child(5n - 4) { transition-delay: 0ms; }
[fc-css-staggered="item"]:nth-child(5n - 3) { transition-delay: 100ms; }
[fc-css-staggered="item"]:nth-child(5n - 2) { transition-delay: 300ms; }
[fc-css-staggered="item"]:nth-child(5n - 1) { transition-delay: 600ms; }
[fc-css-staggered="item"]:nth-child(5n - 0) { transition-delay: 1000ms; }
} */
One honest limit worth knowing: everything here assumes every row has the same number of columns (it can change per breakpoint, just not row to row). Rows with mixed column counts break the :nth-child math — a separate puzzle for another day.