Web Animations Medium
Stacking Cards on Scroll
Sticky cards that scale down and fade as the next card stacks on top, driven by ScrollTrigger and Lenis.
Open in Lab
MCP
html css gsap lenis javascript
Targets: JS HTML
Code
[hidden] {
display: none !important;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: system-ui, sans-serif;
background: #0c0c10;
color: #f2f0f8;
}
.top,
.bottom {
min-height: 40vh;
display: grid;
place-content: center;
text-align: center;
padding: 2rem;
color: #8a8696;
}
.top h1 {
margin: 0 0 0.5rem;
color: #fff;
}
.stack {
max-width: 520px;
margin: 0 auto;
padding: 0 1.25rem 50vh;
}
.card {
position: sticky;
top: 12vh;
margin-bottom: 28vh;
padding: 2rem 1.75rem;
border-radius: 20px;
background: linear-gradient(145deg, #1e1c28, #141218);
border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.45);
transform-origin: center top;
}
.card h2 {
margin: 0 0 0.5rem;
font-size: 1.5rem;
}
.card p {
margin: 0;
color: #a8a4b8;
line-height: 1.55;
}
.card:nth-child(1) {
z-index: 1;
}
.card:nth-child(2) {
z-index: 2;
}
.card:nth-child(3) {
z-index: 3;
}
.card:nth-child(4) {
z-index: 4;
}const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const cards = document.querySelectorAll("[data-card]");
gsap.registerPlugin(ScrollTrigger);
function initStack() {
cards.forEach((card, i) => {
if (i === cards.length - 1) return;
if (reduced) return;
gsap.to(card, {
scale: 0.88,
opacity: 0.45,
filter: "brightness(0.75)",
ease: "none",
scrollTrigger: {
trigger: cards[i + 1],
start: "top 85%",
end: "top 15%",
scrub: true,
},
});
});
}
function boot() {
if (!reduced && typeof Lenis !== "undefined") {
const lenis = new Lenis({ autoRaf: false });
lenis.on("scroll", ScrollTrigger.update);
gsap.ticker.add((t) => lenis.raf(t * 1000));
gsap.ticker.lagSmoothing(0);
}
initStack();
ScrollTrigger.refresh();
}
boot();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Stacking Cards</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<header class="top">
<h1>Stack on scroll</h1>
<p>Each card shrinks as the next one arrives.</p>
</header>
<div class="stack">
<article class="card" data-card>
<h2>Capture</h2>
<p>Collect signals from every channel in one pipeline.</p>
</article>
<article class="card" data-card>
<h2>Shape</h2>
<p>Normalize events into a schema your team trusts.</p>
</article>
<article class="card" data-card>
<h2>Ship</h2>
<p>Push insights to dashboards without another ETL job.</p>
</article>
<article class="card" data-card>
<h2>Repeat</h2>
<p>Automate the loop and keep latency low.</p>
</article>
</div>
<footer class="bottom">Done</footer>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/ScrollTrigger.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/lenis.min.js"></script>
<script src="script.js"></script>
</body>
</html>Stacking Cards on Scroll
Each card sticks while the following card scrolls over it, with scale and opacity tied to scroll progress — ideal for feature lists and pricing tiers.