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. Here’s that CSS — --_cards---number-of-columns is Webflow’s generated name for the number-of-columns variable, and the config block at the top is the only part you’ll actually tweak:

<!-- Embed on the page (or in Site head) -->
<style>
  [data-animate-item].in-view {
    /* ==========================================
     STAGGERED ANIMATION CONFIGURATION
     ========================================== */

    /*
     --animation-mode: Switches between linear and exponential pacing.
     - Effect: Blends the linear progression with the exponential deceleration.
     - Range: 0 to 1.
       * 0   = Pure Linear (equal time gaps between every single card).
       * 1   = Pure Exponential (gaps decrease over time; index 0 is always 0s).
       * 0.5 = A smooth hybrid (soft dampening towards the end of the row).
  */
    --animation-mode: 1;

    /*
     --step-or-max-delay: Dual-purpose time multiplier.
     - Effect: Defines the overall pacing and speed of the stagger.
     - Range: 0.05s to 0.5s for Linear step; 0.2s to 1.5s for Expo max ceiling.
       * Linear mode: Acts as the fixed step between cards (e.g., 0.2s -> 0s, 0.2s, 0.4s...).
       * Expo mode:   Acts as the theoretical maximum delay ceiling for the last column.
  */
    --step-or-max-delay: 0.5s;

    /*
     --expo-base: Controls the deceleration curvature (only active when --animation-mode is > 0).
     - Effect: A lower value makes subsequent items animate much closer together (faster deceleration).
     - Range: 0.1 to 0.9. (Recommended: 0.3 to 0.6).
       * 0.1 = abrupt deceleration; the last items trigger almost instantly after the second.
       * 0.9 = very subtle deceleration; behaves almost like a straight linear progression.
  */
    --expo-base: 0.5;

    /* ==========================================
     MATHEMATICAL CALCULATION
     ========================================== */

    /* 0-based column index per row */
    --index: mod(sibling-index() - 1, var(--_cards---number-of-columns));

    /* 1. Linear pacing component */
    --linear-part: var(--index);

    /* 2. Exponential pacing component */
    --expo-part: calc(1 - pow(var(--expo-base), var(--index)));

    /* 3. The Interpolation Formula (Linear Interpolation / Lerp) */
    --final-curve: calc((var(--linear-part) * (1 - var(--animation-mode))) + (var(--expo-part) * var(--animation-mode)));

    /* Final computed transition delay */
    transition-delay: calc(var(--final-curve) * var(--step-or-max-delay));
  }
</style>

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 CSS above is the exact block from the cloneable — tune the three config variables at the top (--animation-mode, --step-or-max-delay, --expo-base) and leave the math section as-is.

Resources

In the knowledge base

Reference pages derived from this lesson — the same material reorganised by concept, so you can look one thing up without rewatching. In English.

  • Scroll-driven animation — scrub versus trigger actions

    Every scroll animation is one of two kinds: a scrub, where the playhead is tied continuously to scroll position, or trigger actions, where crossing a threshold fires an animation that then plays on its own clock; choosing wrong is the most common cause of a scroll effect that feels off, and the scaffold for a scrub is always a tall section with a sticky full-viewport window inside it.

  • Stagger — spreading one animation across many targets

    Stagger spreads a single animation across multiple targets, so it does nothing at all with one target; offset time sets a fixed gap between each target's start while total time is one budget divided across the intervals, and the stagger's own ease shapes the distribution of delays — a different job from the action's ease.

  • Stagger a grid row by row as it scrolls into view

    GSAP's own stagger cannot do this, so invert the responsibility — a set action adds an in-view class to each card as it enters the viewport, CSS transitions do the animation, and a per-card transition-delay computed from sibling-index() and a column-count variable produces a stagger that resets on every row and re-derives itself at every breakpoint.

  • A style cleanup deletes the classes your interactions toggle

    A class that exists only to be added by a GSAP set action is applied to no element, so Webflow flags it as unused and any style cleanup deletes it — publishing itself does not strip it, which means this breaks work that was already correct, later, when someone tidies the project; keep an element wearing the class so it is genuinely used and never appears in a cleanup list.

  • It works in the Designer but not live — or the reverse

    Custom code, non-standard CSS properties, first-paint flicker, scroll lock and keyboard focus all behave differently in the Designer and in plain preview than on the published site, and the Designer additionally shows you your animations' initial states, so half-hidden content while building is expected rather than broken.

  • Verified identifiers

    Every exact attribute name, class name, variable name, SVG id and selector used across the GSAP-in-Webflow corpus, each traced to the cloneable it was copied from; copy these verbatim and never reconstruct one from memory, because a wrong identifier fails silently.

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.