HomeBlogWallet stacking

Field notes

Wallet stacking: how to build the scroll effect everyone keeps copying.

One animation has been quietly doing the heavy lifting on Apple product pages, Stripe, Linear, and about half the sites that win Awwwards. It has no agreed-upon name, needs zero libraries, and runs on roughly 60 lines of code.

SCROLL INTERACTION Wallet Stacking The scroll effect everyone keeps copying. 01 · PRINCIPLE Depth Layers read as space. 1
60 linesThe entire effect, start to finish
0 librariesNo GSAP, no Lenis, no plugins
2 elementsDoing ninety percent of the work
3 to 6 cardsPast that, it stops earning it

See it live firstThe finished effect is running on CodePen. Scroll it, then come back and I will show you how it is built.

View the live demo on CodePen →

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.

SCROLL 20% card 1 rising SCROLL 60% stacking SCROLL 100% full deck The frame never moves. Scroll position alone drives the pile, forward and back.
Fig. 1 - The same pinned frame at three points in the scroll. The frame never moves, only the cards do.

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.

The cardinal rulePin, do not hijack. If the reader loses control of their own thumb, the effect has already failed.
  1. 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."
  2. 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.
  3. 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.

How the pin works A tall wrapper creates the scroll distance. A sticky frame holds still while it passes. .scene height: (n + 1) x 100vh .stage position: sticky height: 100vh native scroll keeps running What stays on screen VIEWPORT (pinned) TOP CARD rises + settles each card enters from the bottom, on scroll
Fig. 2 - Two boxes, one trick. The wrapper scrolls. The stage does not.
<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.

One number, sliced four ways Each card owns an equal share of the scroll, and animates only inside it. card 1 card 2 card 3 card 4 p = 0 0.25 0.50 0.75 p = 1 you are here Cards 1 and 2 have landed. Card 3 is mid-rise. Card 4 has not been asked to do anything yet. Scroll back up and every one of those states runs in reverse, exactly.
Fig. 3 - The scroll bound to a number. Each card animates only inside its own slice, which is why the effect scrubs cleanly in both directions.

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))`;
});
The two numbers you will actually tune FRAME TOP CARD settles here PEEK 22px. The strip you leave showing. Set it to 0 and the pile disappears. card waiting below the frame ENTRY_Y about 85% of viewport height The ease-out is what makes a card settle instead of snap. It is doing more for the feel than anything else here.
Fig. 4 - 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; }
}
MOTION ALLOWED pinned, stacked REDUCED MOTION card one card two card three Same markup, same content, no pinning. Nobody loses the information.
Fig. 5 - The reduced-motion fallback. Do this and you never have to feel bad about shipping the effect.

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