Scroll Stack Cards

£5

Full-width panels that stack over one another as you scroll, each one locking into place with an overshoot while the cards beneath compress into a visible deck.

ScrollTrigger Lenis advanced
5 more details
scroll-stackpinningscrubvelocitystagger
Scroll Stack Cards - GSAP animation effect preview

This demo reads better at your own screen size than in the frame below:

About this effect

A scroll-stacking card effect that builds a visible deck instead of scrolling content past. Each panel rides up from below a pinned frame, overshoots its resting line by a few pixels and locks, while every card already down slides back a notch, scales, dims and leans away one level at a time.

Read the full effect overview

All of it comes off a single scrubbed ScrollTrigger timeline, so the whole deck is scrubbable in both directions with no state to keep in sync. Scroll velocity adds a second layer: a hard wheel flick tips the entire pile back, drags it behind the gesture and squashes it, then springs it level again.

A corner chip counts the cards as they lock, and every distance is measured through functions with invalidateOnRefresh so a resize or a mobile address bar re-fits the deck rather than drifting.

What's included

10 items
  • Builds a visible deck of panels in one pinned section, driven entirely by scroll position
  • Locking overshoot on every arrival: the card rides a few pixels past its resting line, then relaxes onto it
  • Depth compression staggered level by level, so the pile settles like a stack of cards rather than one block
  • Three depth channels off one number: scale step, dim overlay and a backward lean per level of depth
  • Velocity lean that tips, drags and squashes the whole deck on a fast flick, always released back to level
  • Live counter and progress rail fed from the scrubbed timeline, so the count flips on the frame a card visibly locks
  • Depth cap and reduced offsets on coarse pointers, so five cards still read as a deck at 375px
  • Reduced motion and no-JS both fall back to the plain vertical column the markup already is, with nothing hidden
  • Configurable through data attributes: peek offset, scale step, dim step, depth cap, scroll length, velocity lean on or off
  • Pins below a fixed page bar by measuring it live, and re-measures every distance on ScrollTrigger refresh

Perfect for

5 use cases
  • Product feature walkthroughs where each panel needs to be read, not scrolled past
  • Agency service or process sections that should feel like a deck being dealt
  • Pricing or plan comparisons that build up rather than sitting side by side
  • Case-study chapters on a portfolio page, one pinned section per project
  • Onboarding or how-it-works steps that stay visible as the next one arrives

How it works

3 sections

One ScrollTrigger pins the frame and scrubs a single GSAP timeline that spends one unit of timeline time per arriving card. The arriving panel travels from below the frame to a few pixels past its resting line, then relaxes onto it; the cards beneath get their depth values as explicit start and end pairs with a stagger, so a mid-scroll resize cannot re-record the deck's start state.

Every distance is recalculated on refresh, so the off-frame start position and the pin length are re-derived rather than cached. Depth is clamped so cards deeper than the cap freeze in place and the deck cannot outgrow a short viewport.

Scroll speed is clamped, mapped to degrees and eased onto the deck as one number, driving a tip, a drag and a vertical squash together; a watchdog releases it back to zero once scrolling stops. The counter and rail read the timeline's own onUpdate rather than the scroll handler, so they track the scrubbed visual state instead of the raw wheel.

Plugins ScrollTrigger
Difficulty Advanced
Smooth scroll Lenis integration
Includes HTML + JS + CSS source, documentation, AI setup prompt, lifetime updates

Lighthouse, as measured

Google Lighthouse on this effect's demo, 9 September 2026. Where a score is below 100 the reason is stated underneath, whether it is a decision or open work.

Accessibility
94
Best practices
100
  • Accessibility: Background and foreground colors do not have a sufficient contrast ratio. Open. Not yet reviewed or fixed, listed here rather than left out of the score.

No performance score, on purpose. That figure depends on how you deploy: your server's compression and caching, your CDN, the connection and the device doing the test, none of which the code controls. The same page can score very differently on two consecutive runs, so measure it where it will live.

Paid effect

Purchase to unlock the code.

Buying Scroll Stack Cards opens the HTML, CSS and JavaScript source, the full documentation, an AI setup prompt for your editor, and every update we ship to it. Standard license: unlimited personal and commercial projects.

£5 Standard license, unlimited projects

Browse free effects

Documentation

Quick Start

1. Add to your HTML <head>:

Code snippet omitted: it ships with the download.

2. Add before the closing </body> tag:

Code snippet omitted: it ships with the download.

3. Add the effect HTML anywhere in your <body>:

Code snippet omitted: it ships with the download.

Two or more [data-stack-card] panels are required; below that the script leaves the markup alone. Any number above that works, the scroll length grows with the card count and every distance is re-measured on refresh.

4. Set the counter's total to your card count. data-stack-count starts as plain text (05 / 05) so the no-JS and reduced-motion document reads correctly. The script overwrites it once it takes over.

Options

All set on the [data-stack] section:

Attribute Values Default Description
data-offset Any number (px) 26 Visible edge of each buried card per level of depth. 0 removes the peek and the deck reads as one card being replaced
data-scale 0 to ~0.09 0.055 Scale removed per level of depth. Past ~0.09 the third card down is small enough to read as a different component
data-dim 0 to 1 0.16 Opacity of black laid over each level of depth, capped at 0.88 in total
data-max-depth Whole number ≥ 1 4 (3 on touch) How many levels keep moving. Deeper cards freeze at this depth, so the deck cannot grow past the top of a short viewport
data-lean true, false true The velocity lean. Always off on coarse pointers regardless of this value
data-scrub Any number 1 Multiplier on the scroll distance each card takes to arrive. 1.5 is a slower, more deliberate deal; 0.6 is brisk
data-stack-bar Any CSS selector none Selector for a fixed page bar. Its height is measured live, the pin starts below it and the frame shrinks to the space left over

On coarse pointers data-offset and data-scale are scaled down automatically (to 55% and 70%) and the depth cap drops, so a five-card deck still reads as a pile at 375px without overflowing a short viewport.

How it works

One ScrollTrigger pins .stack__viewport and scrubs a single timeline that spends one unit of timeline time per arriving card. Within a unit, the arriving panel gets a fromTo from below the frame to a few pixels past its resting line (power2.out) plus a short relax onto the line, that overshoot is the lock. Simultaneously, every card already down gets its next depth values as an explicit from/to pair with a stagger, nearest card first, so the pile compresses level by level.

The from-values are explicit rather than inherited from whatever the previous step left, so invalidateOnRefresh can re-measure after a resize without the deck re-recording a mid-scroll state as its start. Card paint order is plain z-index; perspective is applied per element (transformPerspective) rather than on an ancestor, because an ancestor with perspective or transform-style: preserve-3d makes the browser paint children by computed depth and ignore z-index, which puts the card leaning furthest back in front of the whole deck.

Velocity comes from ScrollTrigger.getVelocity(), clamped and mapped to degrees, then pushed through gsap.quickTo with a back.out ease so it eases in rather than jittering and springs past level on the way back to zero. A gsap.ticker watchdog releases the lean once scroll updates stop, which is the only thing that would otherwise leave the last reading applied.

Accessibility

  • Reduced motion: prefers-reduced-motion: reduce skips the pin entirely. The panels stay the plain, fully visible vertical column the markup already is, and the counter shows the final value. Nothing is animated, pinned or hidden.
  • Touch devices: the mechanic is scroll-driven, so it works as-is. Depth offsets shrink, the visible depth is capped and the velocity lean is dropped, touch inertia makes the velocity reading noisy enough that the lurch reads as a bug rather than a flourish.
  • No JavaScript: the base stylesheet is the readable column. The only thing gated behind the script's .stack--live class is the pinned layout, and the velocity half of the cue stays hidden because it is not true without the script.
  • Screen readers: cards are <article> elements in reading order with real headings; the progress rail and dim overlays are aria-hidden.

Dependencies

Required:

  • GSAP 3.12+
  • ScrollTrigger plugin

Optional:

  • Lenis (smooth scroll). The scrubbed deck reads noticeably smoother with it; the script wires lenis.on('scroll', ScrollTrigger.update) automatically when Lenis is present and works on native scroll when it is not.

Worked examples, the events and programmatic API, and the class reference ship with the download, alongside the full source.

Your cart

Your cart is empty

The Vault £99

Everything in the catalogue, plus everything we release next.