Responsive Staggered Scroll Animation in Webflow

Advanced 28:12 webflowgsapscrollcssresponsive

Build a responsive staggered scroll animation in Webflow: cards fade and slide in row by row, with the stagger adapting to the column count at every breakpoint.

Key takeaways

  • GSAP's built-in stagger can't do "per-row on scroll": target the trigger and there are no siblings to stagger; target the whole attribute group and it staggers every card at once, even off-screen ones.
  • The fix: GSAP only adds an in-view class as each card enters the viewport; CSS transitions animate the state change, and a per-card transition-delay creates the stagger.
  • Compute each card's delay from its position in the row with the sibling-index() CSS function and a Webflow variable for the column count, so the stagger resets on every row.
  • Store the column count in a variable with a mode per breakpoint, and the same stagger stays correct as the grid reflows (3 / 3 / 2 / 1 columns).
  • Keep the cards editable in the Designer with an override rule (visible when the JS class is absent), since the initial state hides them.
  • The whole effect is tiny — one set action, one utility class, some CSS, and one variable — and it works for static elements too.

Video chapters

  1. 00:00 Intro
  2. 01:05 Explaining the structure of the section
  3. 07:45 Breaking down the foundations of the scroll interaction
  4. 16:04 Implementing the interaction: initial and final states
  5. 21:05 Implementing the interaction: responsiveness and transition delay
  6. 27:03 Outro

This closing lesson tackles a problem that isn’t obvious until you hit it: staggering cards per row as they scroll into view, while keeping the stagger correct as the grid reflows across breakpoints. It’s a fitting finale for the course, because it leans on almost everything we’ve built — a scroll trigger, the “GSAP toggles a class, CSS animates” pattern, Webflow variables and variable modes — plus one modern CSS function that ties it all together.

The elegant part is how little code it takes: one set action, one utility class, a bit of CSS, and a single variable.

How it works

Why GSAP’s stagger doesn’t fit. You’d expect an animate action with move-Y, opacity, and stagger enabled. But there’s no good target. Point the action at the trigger element and every card animates in isolation — a single element has no siblings to stagger against. Point it at the shared data-animate-item attribute and, the first time it fires, GSAP staggers the entire collection at once, including cards far below the fold. Neither produces a per-row stagger tied to what’s actually entering the viewport.

The strategy: class + CSS delay. So GSAP does the minimum — a set action that adds an in-view class to each card as it scrolls in (scroll interaction, trigger actions, enter → play, threshold when the item’s top hits ~90% of viewport height). The animation lives in CSS: the card’s initial state is opacity: 0 and translateY(20%); the in-view class is the final state (opacity: 1, translateY(0)) plus two transitions (transform + opacity, 800ms, expo). The stagger is then just a per-card transition-delay.

Responsive delay via sibling-index() + a variable. The delay has to depend on a card’s position within its row, and rows change size across breakpoints. Store the column count in a Webflow variable (number-of-columns) with a variable mode per breakpoint (3 desktop / 3 tablet / 2 landscape / 1 portrait). In custom CSS, use the modern sibling-index() function together with that variable to compute each card’s index within the row, then convert it into a transition-delay. Because the formula reads the variable, the same rule re-derives correct delays whenever the grid reflows — fully responsive, no rebuild. The CSS exposes a few friendly knobs: animation-mode (0 = linear, 1 = exponential, in between = a blend), a step/max-delay, and an expo-base for the curve; the “mathematical” section below those can stay untouched.

Staying editable in the Designer. Since the initial state hides the cards, they’d vanish while you build. An override rule shows them whenever Webflow’s runtime JS class isn’t on the html element — a condition only true inside the Designer — so editing stays normal and the live animation is unaffected. (As always, keep the in-view class registered in a style guide so Webflow doesn’t prune it.)

How to use it

  1. Layout + trigger. A CMS (or static) grid where each item has data-animate-item. On the card item, set the initial state: opacity: 0, translateY(20%).

  2. Scroll interaction. Target data-animate-item, trigger actions, enter → play, start threshold ~90% viewport height. One set action adding the in-view class to the trigger element.

  3. The in-view state. Define an in-view class with opacity: 1, all move transforms 0, and transitions on transform + opacity (800ms, ease-out-expo). Register it in your style guide.

  4. Responsive column variable. Create a number variable number-of-columns and a variable mode per breakpoint with the right values (e.g. 3/3/2/1).

  5. The stagger engine (CSS). Add an embed with the CSS that targets [data-animate-item].in-view, computes each card’s row index from sibling-index() and number-of-columns, and sets a transition-delay from it. Tune animation-mode, the step/max-delay, and expo-base to taste. Add the Designer-visibility override. Publish and test across breakpoints.

The stagger CSS (the delay formula and its config block) is pasted from the cloneable — it’s heavily commented there. Copy it and adjust the config values rather than rebuilding the math.

Resources

Frequently asked questions

Why can't I use GSAP's stagger for a scroll-in animation that staggers each row?
Because neither target works. Set the action's target to the trigger element and each card animates alone — no siblings to stagger. Target the shared attribute and GSAP staggers the entire collection the first time it fires, including cards that haven't entered the viewport yet. Neither gives a clean per-row stagger.
How do you stagger cards row by row on scroll in Webflow, then?
GSAP only adds an in-view class to each card as it enters the viewport (a simple set action). The animation itself is a CSS transition between an initial state (opacity 0, moved down) and the in-view state, and a per-card transition-delay produces the staggered timing.
How do I make the stagger delay responsive to the number of columns?
Store the column count in a Webflow variable and add a variable mode for each breakpoint (e.g. 3/3/2/1). In custom CSS, compute each card's index within its row using the sibling-index() function and that variable, then turn the index into a transition-delay. Because the delay reads the variable, it re-derives correctly whenever the grid reflows.
How do I keep the cards visible while editing in the Webflow Designer?
The initial state (opacity 0) hides them in the Designer too. Add a CSS override that forces items with the animate attribute back to visible when Webflow's JS class isn't present on the html element — that condition is only true inside the Designer, so the live animation is unaffected.

← Back to the course