See it live firstThe finished effect is running on CodePen. Scroll it, then come back and I will show you how it is built.
I started building this thing for clients years ago, back when the only way to pull it off cleanly involved a scroll library, a pile of math, and a prayer. Every time I handed it off I hit the same wall: I did not know what to call it. "Sticky scroll-pinned card reveal" is not a name. It is an apology.
So here is the name, and I think it has been sitting in your pocket the whole time.
Open Apple Wallet. Your cards are stacked and overlapping, each one showing a thin strip of the card behind it, the active one snapped to the front. That single piece of UI taught a whole generation what a digital card stack looks like. The scroll effect borrows its entire grammar. So I am calling it wallet stacking, and once you see it that way you cannot unsee it.
This post is as close to copy and paste as it gets. Markup, styles, script, and the exact mechanics underneath. Even if you never ship a single card, the two techniques here, pinning and scroll-linked animation, are a blueprint for nearly any scroll story worth telling.
The effect
What you are actually building.
Job: turn three to five sequential ideas into one held breath
The visitor scrolls. The frame appears to freeze in place. Then a set of cards rises up from the bottom, one after another, and settles into a neat overlapping pile, each new card sliding in front of the last with just the top edge of the older ones peeking out. When the last card lands, the frame releases and the page moves on.
It reads as depth and sequence at the same time, which is exactly why product teams reach for it when they have three to five ideas that build on each other.
Step 01
Start with principles, not plugins.
Job: get the three rules right so the code cannot betray you later
The tactics below are fun, but these three principles are the whole game. Get them wrong and no amount of code will save you.
- Pin, do not hijack. Do not intercept the wheel, do not animate on a timer, do not fight the user's thumb. Let native scroll run exactly as it always does and simply make one frame sticky while it passes. The reader keeps full control of speed and direction. That is the difference between "elegant" and "get me off this page."
- Minimum effective motion. Borrowed from Tim Ferriss's minimum effective dose: the smallest movement that reads is the correct movement. A card that rises and settles with a slight shrink behind the next one is plenty. Rotations, bounces and parallax on every layer are how you turn a wallet into a slot machine.
- One idea per card. Three to six cards, each carrying a single thought. This effect is a spotlight. Point it at a short sequence that actually earns the theater, then give people an ordinary unpinned page on either side so it feels like a held breath rather than a hostage situation.
If your content is a long list, or anything people want to skim, search or scan, wallet stacking is the wrong tool. Pinning takes the scrollbar away for a moment. Only spend that on a moment worth it.
Most common missReaching for a smooth-scroll library before you have written a single line. The effect does not need one, and the library will hide the mistake you are about to make instead of preventing it.
Step 02
Build the skeleton.
Job: two elements, no JavaScript, and the pin already works
The entire illusion rests on two boxes. A tall outer wrapper that creates scroll distance, and a sticky inner stage exactly one screen tall that holds still while the wrapper scrolls past.
<section class="scene"> <!-- tall: (cards + 1) x 100vh -->
<div class="stage"> <!-- sticky, 100vh, overflow hidden -->
<article class="card">...</article>
<article class="card">...</article>
<article class="card">...</article>
</div>
</section>
.scene { height: calc((var(--n) + 1) * 100vh); position: relative; }
.stage { position: sticky; top: 0; height: 100vh; overflow: hidden; }
That is the trick in two lines. The wrapper gives you room to scroll. The sticky stage stays put. overflow: hidden lets cards slide up into the frame from below without the page growing sideways. You have not written a single line of JavaScript and you already have a pinned scene.
Most common missA parent with overflow: hidden somewhere up the tree. Sticky silently stops working and you will spend an hour blaming your math.
Step 03
Turn scroll into a single number.
Job: reduce the whole animation to one variable between 0 and 1
Everything else is one question: how far through the scene are we?
const rect = scene.getBoundingClientRect();
const scrollable = scene.offsetHeight - window.innerHeight;
const p = Math.min(Math.max(-rect.top / scrollable, 0), 1); // 0 -> 1
This is the important idea, and it is what separates a good version from a janky one. The animation is not merely triggered by scroll, it is bound to it. Scroll down and the deck builds. Scroll back up and it unbuilds, in reverse, frame for frame. You are scrubbing a timeline with your thumb.
Most common missFiring the animation once when the section enters the viewport. That gives you a one-way effect that looks broken the moment someone scrolls back up.
Step 04
Give each card its slice.
Job: move a card from below the frame to its resting spot on the pile
Hand every card its own portion of that 0-to-1 progress. Inside its slice, move it from below the frame up to its resting spot.
cards.forEach((card, k) => {
const restY = (k - (cards.length - 1) / 2) * PEEK; // final stacked spot
const local = Math.min(Math.max((p - k / cards.length) * cards.length, 0), 1);
const e = 1 - Math.pow(1 - local, 3); // ease out
const y = ENTRY_Y + (restY - ENTRY_Y) * e; // rise from below
card.style.transform = `translate(-50%, calc(-50% + ${y}px))`;
});
PEEK is the strip of each buried card you leave showing. ENTRY_Y is how far below the frame each card waits. Those two numbers are most of the personality.Most common missA linear ease. It is technically correct and it feels like a spreadsheet. Cube the ease-out and the same code suddenly feels expensive.
Step 05
Add depth, with restraint.
Job: make the pile read as three-dimensional without anyone noticing why
As newer cards land in front, nudge the older ones back a hair and dim them. This is minimum effective motion in action. Barely visible, and it makes the pile feel three-dimensional.
let buried = 0;
for (let j = k + 1; j < cards.length; j++) {
buried += Math.min(Math.max((p - j / cards.length) * cards.length, 0), 1);
}
const scale = 1 - buried * 0.03;
const bright = Math.max(1 - buried * 0.08, 0.6);
Multiply those into the transform and filter. If you can feel the shrink happening, you have gone too far. Cut it in half.
Most common missStacking depth on top of rotation on top of parallax. Each one is defensible alone. Together they are a slot machine.
Step 06
Respect the people who did not ask for a ride.
Job: collapse the whole thing to a plain readable stack, in four lines
This is the part most tutorials skip and every real site needs.
@media (prefers-reduced-motion: reduce) {
.scene { height: auto; }
.stage { position: static; height: auto; overflow: visible; }
.card { position: relative; transform: none !important; }
}
Most common missTreating reduced motion as a nice-to-have you will add later. It is four lines. Later never comes.
The whole thing
A tall box, a sticky box, and some arithmetic.
That is wallet stacking, start to finish. A tall wrapper, a sticky frame, and a bit of math that turns pixels of scroll into position on a pile. Change the card count and it re-sizes itself.
Fork the full code on CodePen →Hit Fork, change the copy and the colours, and make it yours.
Tools
What I actually use.
- No framework (free)
- You do not need one. The version here is vanilla HTML, CSS and JavaScript.
- GSAP ScrollTrigger (free tier)
- If GSAP is already in the project, its pin feature makes the sticky part a one-liner and handles edge cases for you. Overkill for just this, worth it if you are doing several scroll scenes.
- Lenis (free)
- Adds buttered smooth scrolling. Pairs nicely with wallet stacking, but test on a trackpad and a mouse wheel before you commit. Smooth scroll is a taste, not a default.
- CodePen (free)
- The fastest place to prototype this, and a good place to share it once it works.
The 48 hour challenge
Ship one card. Just one.
Do not build the whole deck. Open a blank CodePen and build the pin: a tall wrapper, a sticky stage, and a single card that rises from the bottom as you scroll. That is Steps 02 through 04, and it is about twenty minutes.
Then do the one thing that decides whether this ends up on a client site or in your bookmarks: scroll back up. If the card unbuilds cleanly in reverse, your progress number is bound correctly and everything else is decoration. If it snaps or sticks, you have a trigger instead of a binding, and no amount of easing will fix it.
Get that one card scrubbing both directions before Sunday night. The other five take an afternoon.
If you build one, send it to me. I will look at every single one, and if it is good I will link it in the next post.
Want an effect like this on your site, done properly?
Wallet stacking is the fun part. The hard part is knowing whether your site needs it at all, or whether it needs a phone number people can actually find. Send me your URL and I will tell you which one you are.
Get a free lead leak review →Or write me directly: mat.steenwinkel@benchdigital.ca