Stack cards
Full width cards that stack on top of one another as the page scrolls.
Documentation
-
Copy to Webflow
Add Geist and Geist Mono to your site's fonts (Site settings, Fonts, Google Fonts) before pasting: Webflow removes any font your site has not installed. -
Add custom CSS
Custom CSS in WebflowThis CSS already ships inside the Webflow embed, so a pasted section needs nothing added. Paste it yourself, in the page or the site custom code, only if the effect runs on several pages. The tutorial explains where.
See the tutorialThree things belong to the page rather than to the component, and the pile needs all three: a card needs an opaque background, or the pile is see through; a card needs a height, or enough content to be tall; and the bottom padding of the track is how long the last card holds the screen before the page moves on. -
Add custom JS
Custom JavaScript in WebflowThis script already ships inside the Webflow embed, settings block included. You paste it yourself, before the closing body tag of the page or the site custom code, only when the effect has to run on several pages, and the tutorial says how to choose.
See the tutorial -
Tune it
How to add a custom attribute in WebflowA custom attribute is a pair, a name and a value, typed in the Settings panel of the element you selected, under Custom attributes. The name is what the script looks for, the value is what it reads. The tutorial shows the panel.
See the tutorialCustom attributesSelect the section on the canvas and open the Settings panel. Under Custom attributes, add a pair: the name on the left, the value on the right.Custom attribute What it does data-stack-cards Marks the track, the block that holds the cards. It takes no value: leave the value field empty. Without it nothing is mounted, and the boot of the embed falls back to the section it was pasted in, which only works when the cards are that section’s own children. data-stack-cards-card Marks one card. It takes no value. Every card carries it, and every card is a direct child of the track. Without it on any of them the engine takes the track’s own children as the cards, leaving out anything holding a script, and says so once in the console; a card that is a grandchild is never found either way. [ Optional ] data-stack-cards-state Written by the engine on the track while the pile is in the window, with the value active, and removed when it leaves. Never written by hand: it is what the stylesheet reads to know it may scale and dim a card, so a pile nobody is looking at carries no transform and no filter. [ Optional ] SettingsEvery setting is a custom attribute too, added the same way and on the same section. An attribute always wins over the defaults written in the code.Attribute Default What it is data-stack-cards-top clamp(12px, 3vh, 40px) Where a card stops, counted from the top of the window; the place for the height of a fixed header. data-stack-cards-step clamp(4px, 0.6vw, 10px) How much lower each card stops than the one before it, which is what gives the pile a visible edge. data-stack-cards-depth 0.08 How much a card shrinks while it is being covered, as a share of its size. data-stack-cards-dim 0.3 How much light a card loses while it is being covered. data-stack-cards-blur 0 How blurred a card is once it is covered, in pixels. data-stack-cards-enter 1 Where the receding starts, as a share of the window height measured from the top: 1 is the bottom edge. data-stack-cards-ease none The shape of the receding across that travel. Options: any GSAP ease name, none (even with the scroll), power1.out, power2.inOut, and so on. data-stack-cards-scrub 0 How long the cards take to catch up with the scroll, in seconds. data-stack-cards-debug false Writes the state, the card count and the pin line in data-stack-cards-debug-* attributes. Options: false, true.
Two files and their pictures (4), no build step. It also needs GSAP 3 and its ScrollTrigger plugin on the page, loaded with the two tags in step 1. Without them the cards still pin, which is CSS; only the receding is missing.
It runs in the browser only. Load it with a script tag in plain HTML, in a
client-side script in Astro, never in the frontmatter. In React, call
StackCards.mount(ref.current) in useEffect and
destroy() in its cleanup.
-
Tune it, either way
Per element with an attribute, or once for the whole site by editingDEFAULTSat the top of the file.Attribute Default What it is Lower Higher data-stack-cards-top clamp(12px, 3vh, 40px) Where a card stops, counted from the top of the window; the place for the height of a fixed header. The cards stop right at the top edge The cards stop further down the screen data-stack-cards-step clamp(4px, 0.6vw, 10px) How much lower each card stops than the one before it, which is what gives the pile a visible edge. The cards land exactly on top of one another A deep deck, and less room for the card at the front data-stack-cards-depth 0.08 How much a card shrinks while it is being covered, as a share of its size. The card stays its size and only darkens The card falls right back, and its edges show around the one above data-stack-cards-dim 0.3 How much light a card loses while it is being covered. The pile stays evenly lit The cards underneath go to black data-stack-cards-blur 0 How blurred a card is once it is covered, in pixels. Sharp all the way down, which costs nothing A soft depth of field, paid for on every frame data-stack-cards-enter 1 Where the receding starts, as a share of the window height measured from the top: 1 is the bottom edge. The card underneath waits, then recedes late and fast The receding starts before the next card is on screen data-stack-cards-ease none The shape of the receding across that travel. Options: any GSAP ease name, none (even with the scroll), power1.out, power2.inOut, and so on. data-stack-cards-scrub 0 How long the cards take to catch up with the scroll, in seconds. 0 locks them to the scroll, frame for frame They lag behind and glide to a stop data-stack-cards-debug false Writes the state, the card count and the pin line in data-stack-cards-debug-* attributes. Options: false, true. -
Drive it from JavaScript
When the cards change after load, when an attribute is changed at runtime, or when the page reacts to the card at the front.const element = document.querySelector('[data-stack-cards]'); element.addEventListener('stackcards:card', (e) => { console.log(e.detail); }); const pile = StackCards.mount(element); // the track auto() mounted, or a new one pile.cards; // the cards, in the order they stack pile.refresh(); // after changing an attribute, or after the cards changed // Cards added after the page loaded: mount everything marked inside it. StackCards.auto(document.body); // When the pile leaves the page: StackCards.destroy(element); // kills the triggers and clears everything it wrote
-
Fit it to your page
- Mark the track and the cards:
data-stack-cardson the block, anddata-stack-cards-cardon each card inside it. The cards are the children of the track, in the order they should stack. - Settings go on the track, as more attributes:
data-stack-cards-depth="0.16",data-stack-cards-top="96px". Three levels, least specific first:DEFAULTS, thenwindow.StackCardsSettings = { depth: 0.16 }declared before the engine, then the track's own attributes. - A fixed header is what
topis for: give it the height of the header and the cards stop under it,data-stack-cards-top="calc(72px + 1rem)". - From JavaScript:
StackCards.mount(el, overrides)returns{ element, settings, cards, refresh(), destroy() }.refresh()reads the attributes again, applies them and lays the triggers out afresh, which is what an attribute changed at runtime needs.StackCards.destroy(el),StackCards.refresh(el),StackCards.auto(root)andStackCards.defaultscomplete it. - Events, all bubbling:
stackcards:readyonce the pile is built ({ element, cards, reduced }),stackcards:enterandstackcards:leaveas the pile comes into the window and leaves it ({ element, cards, top }), andstackcards:cardeach time another card takes the front ({ element, index, card, cards }). - **Under
prefers-reduced-motion: reduce** nothing pins and nothing recedes: the cards follow one another down the page at full size and full light, and no scroll trigger is created at all.stackcards:readystill fires, withreduced: true. - Two piles on one page are two mounts with their own settings, their own triggers and their own events. Destroying one leaves the other running.
- The preview ships whole.
snippet.html, the pictures ofimages/and the last part ofstack-cards.css, after the linelook, left out of the Webflow Embed, are the preview: the same cards, words, pictures and ground, in Geist and Geist Mono, loaded by the font tag of the Code tab. Nothing of the demo is left out. Thesc-*classes are never read by the engine: on cards of your own, keep your classes, add the attributes, and drop that last part of the file and the font tag. - On Webflow, add the fonts first. Add Geist and Geist Mono to your site's fonts (Site settings, Fonts, Google Fonts) before pasting: the paste names them, and the Designer drops a font the site does not have.
- The words of the markup are the preview's, and they ship as they are: pasted, the page reads the way the preview did. Put the user's own content in their place whenever they want, in the page's language.
- Mark the track and the cards:
-
Avoid the pitfalls
- An ancestor that cuts its overflow kills the pin.
overflow: hiddenon a parent makes it the scroll container of every sticky card inside it, and a container that does not scroll has nowhere to pin. Nothing errors, the cards simply scroll away. Useoverflow-x: clip, which cuts the same overflow without creating a scroll container. The engine walks up the tree at mount and names the ancestor in the console. - A card needs an opaque background. The pile is cards laid over cards: a transparent card shows the one underneath through itself and the effect reads as a bug.
- The track's own layout is yours. Flex column with a gap, or blocks with margins, both work. What must not happen is a
displaythat takes the cards out of the flow (absolute, a grid where every card sits in the same cell): sticky needs a card to have a place in the flow and a parent taller than it. - The last card holds the screen for as long as the track has bottom padding, and not a pixel more. With none, it reaches its line and leaves in the same movement.
- **Webflow loads its own GSAP, and
Webflow.destroy()kills every ScrollTrigger on the page**, this engine's included. The pin does not care, being CSS, so the pile keeps working and only the receding would stop; the engine looks at itself 0.7 s, 2.2 s and 6 s after its build and puts its triggers back when they have been taken away. A page that callsWebflow.destroy()later than that gets the pin without the receding, andStackCards.refresh(el)brings it back. - The stylesheet is what pins. Without
stack-cards.cssnothing is sticky, nothing recedes, and the engine says so in the console: the sheet carries the mechanics, the engine only drives them. - The settings are custom properties, so a page-wide rule of yours on
--stack-cards-depthand its siblings changes the pile as surely as an attribute does, and loses to an attribute the engine wrote. - **
position: fixedinside a card** is relative to the card while the pile is on screen, and to the window when it is not: a transform makes a containing block, and the transform only exists while the pile is active. Put a fixed layer outside the track.
- An ancestor that cuts its overflow kills the pin.
One click on Copy the AI prompt copies everything an assistant needs to build this effect: the complete code, the markup, the dependency, every setting and every pitfall.
What it does
Stack cards is a scroll animation for Webflow and vanilla JavaScript, built on GSAP 3 and ScrollTrigger. Also called sticky stacking cards, stacking cards on scroll or stacked cards scroll.
Full width cards that stack on top of one another as the page scrolls. Each card stops under the one before it and holds the screen while the next one climbs over it; the card underneath falls back, a little smaller and a little darker, the way a card is put down on a table. At the end of the pile the section lets go and the page carries on. The pin is the page's own sticky positioning, not a scripted one: a sticky card never leaves the flow, so the page is exactly as tall before, during and after, and there is no spacer to insert and no height to hand back, which is where a scripted pin jumps. GSAP and ScrollTrigger drive one number per card, how covered it is, and the stylesheet turns it into a scale, a brightness and a blur. The content of a card stays live the whole way: reading order, tab order, selection and search are the ones the markup has. Pattern type stacked cards, sticky cards, card deck on scroll.
Updated
Questions
Add the custom attribute data-stack-cards to the block that holds the cards and data-stack-cards-card to each card, under Custom attributes in the Settings panel. The cards have to be children of that block, in the order they should stack: a card that is a grandchild is never found.
The pin is CSS position: sticky, not ScrollTrigger's pin, so no spacer is inserted and the page is as tall pinned as it is at rest. GSAP 3.12.5 and ScrollTrigger, loaded from the CDN, only drive how much a covered card shrinks, darkens and blurs. Without them the cards still pin.
Look for an ancestor with overflow: hidden: it becomes the scroll container of every sticky card inside it, and a container that does not scroll has nowhere to pin. Use overflow-x: clip on that ancestor. The engine walks up the tree at mount and names it in the console.
data-stack-cards-depth is how much a covered card shrinks, 0.08 by default, and data-stack-cards-dim is how much light it loses, 0.3 by default. Lower values keep the card its size and the pile evenly lit; higher values make it fall right back and go to black. data-stack-cards-blur adds a blur in pixels and is 0 by default.
Give top the height of the header, for example data-stack-cards-top="calc(72px + 1rem)", and the cards stop under it. top is where a card stops, counted from the top of the window, and takes any CSS length; its default is clamp(12px, 3vh, 40px).