Stacking cards
Cards stick near the top and pile up as you scroll, each one settling slightly lower than the last, with earlier cards shrinking back.
CSSJavaScriptIntermediate
<div class="space">Scroll down</div>
<div class="stack" id="stack">
<div class="card c0" style="--i:0"><span>01</span><h2>Plan</h2></div>
<div class="card c1" style="--i:1"><span>02</span><h2>Design</h2></div>
<div class="card c2" style="--i:2"><span>03</span><h2>Build</h2></div>
<div class="card c3" style="--i:3"><span>04</span><h2>Launch</h2></div>
<div class="card c4" style="--i:4"><span>05</span><h2>Grow</h2></div>
</div>
body{display:block;height:auto}
.space{height:50vh;display:grid;place-items:center;color:#a6938f;font-size:14px}
.stack{width:min(420px,90vw);margin:0 auto;padding-bottom:30vh}
.card{position:sticky;top:calc(24px + var(--i) * 22px);height:230px;margin-bottom:36px;border-radius:18px;padding:26px;
display:flex;flex-direction:column;justify-content:space-between;color:#270100;transform-origin:50% 0;will-change:transform;
box-shadow:0 -10px 30px rgba(0,0,0,.35)}
.card h2{margin:0;font-size:2rem}.card span{font-weight:700;opacity:.6}
.c0{background:#f08a5d}.c1{background:#f3d9c8}.c2{background:#e8b04c}.c3{background:#d98a8a}.c4{background:#b07a6a}
const cards = [...document.querySelectorAll('.card')];
function update() {
cards.forEach((card, i) => {
const next = cards[i + 1];
if (!next) return;
// how far the next card has climbed over this one (0 to 1)
const gap = next.getBoundingClientRect().top - card.getBoundingClientRect().top;
const p = 1 - Math.min(Math.max((gap - 22) / 250, 0), 1);
card.style.transform = `scale(${1 - p * 0.06})`;
card.style.filter = `brightness(${1 - p * 0.25})`;
});
}
addEventListener('scroll', () => requestAnimationFrame(update), { passive: true });
update();
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Stacking cards</title>
<style>
*{box-sizing:border-box}
html,body{margin:0;height:100%}
body{display:grid;place-items:center;background:#140807;color:#fffffc;font:16px/1.5 system-ui,-apple-system,"Segoe UI",sans-serif}
body{display:block;place-items:normal;height:auto}
.space{height:50vh;display:grid;place-items:center;color:#a6938f;font-size:14px}
.stack{width:min(420px,90vw);margin:0 auto;padding-bottom:30vh}
.card{position:sticky;top:calc(24px + var(--i) * 22px);height:230px;margin-bottom:36px;border-radius:18px;padding:26px;
display:flex;flex-direction:column;justify-content:space-between;color:#270100;transform-origin:50% 0;will-change:transform;
box-shadow:0 -10px 30px rgba(0,0,0,.35)}
.card h2{margin:0;font-size:2rem}.card span{font-weight:700;opacity:.6}
.c0{background:#f08a5d}.c1{background:#f3d9c8}.c2{background:#e8b04c}.c3{background:#d98a8a}.c4{background:#b07a6a}
</style></head>
<body>
<div class="space">Scroll down</div>
<div class="stack" id="stack">
<div class="card c0" style="--i:0"><span>01</span><h2>Plan</h2></div>
<div class="card c1" style="--i:1"><span>02</span><h2>Design</h2></div>
<div class="card c2" style="--i:2"><span>03</span><h2>Build</h2></div>
<div class="card c3" style="--i:3"><span>04</span><h2>Launch</h2></div>
<div class="card c4" style="--i:4"><span>05</span><h2>Grow</h2></div>
</div>
<script>
const cards = [...document.querySelectorAll('.card')];
function update() {
cards.forEach((card, i) => {
const next = cards[i + 1];
if (!next) return;
// how far the next card has climbed over this one (0 to 1)
const gap = next.getBoundingClientRect().top - card.getBoundingClientRect().top;
const p = 1 - Math.min(Math.max((gap - 22) / 250, 0), 1);
card.style.transform = `scale(${1 - p * 0.06})`;
card.style.filter = `brightness(${1 - p * 0.25})`;
});
}
addEventListener('scroll', () => requestAnimationFrame(update), { passive: true });
update();
</script>
</body></html>
Good for
Feature lists, process steps and case study stacks.
Watch out for
Each card needs a bigger sticky top offset than the one before. Add scroll space after the last card so the stack can finish.
Accessibility
Wrap motion in @media (prefers-reduced-motion: reduce) and switch it off or shorten it for people who ask for less movement.