Web Animations Medium
Card Stack Cascade
Stacked cards fan out and rearrange into a grid as you scroll, driven by GSAP ScrollTrigger scrub.
Open in Lab
MCP
gsap scrolltrigger perspective
Targets: JS HTML
Code
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--bg: #070a12;
--text: #f0f4fb;
--muted: #8a95a8;
--accent: #86e8ff;
--neon-purple: #ae52ff;
}
html {
background: var(--bg);
color: var(--text);
font-family: "Inter", "SF Pro Display", system-ui, sans-serif;
}
body {
overflow-x: hidden;
}
.intro {
min-height: 70vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 2rem;
}
.eyebrow {
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.18em;
color: var(--accent);
margin-bottom: 1rem;
}
h1 {
font-size: clamp(2.2rem, 6vw, 4rem);
font-weight: 700;
letter-spacing: -0.03em;
background: linear-gradient(135deg, #fff, var(--accent), var(--neon-purple));
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.subtitle {
font-size: clamp(0.9rem, 2vw, 1.05rem);
color: var(--muted);
max-width: 460px;
margin-top: 0.75rem;
line-height: 1.6;
}
/* Stack section */
.stack-section {
position: relative;
min-height: 250vh;
}
.perspective-container {
position: sticky;
top: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
perspective: 1200px;
}
.card-stack {
position: relative;
width: 380px;
height: 460px;
}
/* Cards */
.stack-card {
position: absolute;
inset: 0;
will-change: transform, opacity;
}
.card-glass {
width: 100%;
height: 100%;
background: linear-gradient(
135deg,
hsla(var(--hue, 200), 30%, 15%, 0.6),
hsla(var(--hue, 200), 20%, 10%, 0.8)
);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid hsla(var(--hue, 200), 40%, 40%, 0.2);
border-radius: 20px;
padding: 2.5rem;
display: flex;
flex-direction: column;
justify-content: center;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), inset 0 1px 0 hsla(var(--hue, 200), 50%, 70%, 0.08);
}
.card-num {
font-size: 3rem;
font-weight: 700;
line-height: 1;
margin-bottom: 1rem;
color: hsla(var(--hue, 200), 60%, 60%, 0.2);
}
.card-glass h3 {
font-size: 1.3rem;
font-weight: 600;
margin-bottom: 0.6rem;
color: hsl(var(--hue, 200) 70% 80%);
}
.card-glass p {
font-size: 0.88rem;
color: var(--muted);
line-height: 1.55;
margin-bottom: 1.2rem;
}
.card-tags {
display: flex;
gap: 0.4rem;
flex-wrap: wrap;
}
.card-tags span {
padding: 0.18rem 0.55rem;
border-radius: 4px;
font-size: 0.65rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
background: hsla(var(--hue, 200), 40%, 30%, 0.25);
color: hsl(var(--hue, 200) 60% 70%);
}
/* Outro */
.outro {
min-height: 50vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 2rem;
}
.outro h2 {
font-size: clamp(1.8rem, 4vw, 3rem);
font-weight: 700;
letter-spacing: -0.02em;
margin-bottom: 1.5rem;
}
.btn-back {
display: inline-block;
padding: 0.7rem 2rem;
border-radius: 999px;
border: 1px solid rgba(134, 232, 255, 0.3);
color: var(--accent);
text-decoration: none;
font: 600 0.85rem / 1 "Inter", system-ui, sans-serif;
transition: all 0.25s ease;
}
.btn-back:hover {
background: rgba(134, 232, 255, 0.08);
border-color: var(--accent);
}
/* Reduced motion */
.reduced-motion .stack-card {
opacity: 1 !important;
transform: none !important;
}
@media (max-width: 640px) {
.card-stack {
width: 300px;
height: 400px;
}
.card-glass {
padding: 1.8rem;
}
}if (!window.MotionPreference) {
const __mql = window.matchMedia("(prefers-reduced-motion: reduce)");
const __listeners = new Set();
const MotionPreference = {
prefersReducedMotion() {
return __mql.matches;
},
setOverride(value) {
const reduced = Boolean(value);
document.documentElement.classList.toggle("reduced-motion", reduced);
window.dispatchEvent(new CustomEvent("motion-preference", { detail: { reduced } }));
for (const listener of __listeners) {
try {
listener({ reduced, override: reduced, systemReduced: __mql.matches });
} catch {}
}
},
onChange(listener) {
__listeners.add(listener);
try {
listener({
reduced: __mql.matches,
override: null,
systemReduced: __mql.matches,
});
} catch {}
return () => __listeners.delete(listener);
},
getState() {
return { reduced: __mql.matches, override: null, systemReduced: __mql.matches };
},
};
window.MotionPreference = MotionPreference;
}
function prefersReducedMotion() {
return window.MotionPreference.prefersReducedMotion();
}
function initDemoShell() {
// No-op shim in imported standalone snippets.
}
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import Lenis from "lenis";
gsap.registerPlugin(ScrollTrigger);
initDemoShell({
title: "Card Stack Cascade",
category: "scroll",
tech: ["gsap", "scrolltrigger", "3d-transforms"],
});
const lenis = new Lenis({ lerp: 0.1, smoothWheel: true });
lenis.on("scroll", ScrollTrigger.update);
gsap.ticker.add((time) => lenis.raf(time * 1000));
gsap.ticker.lagSmoothing(0);
const reduced = prefersReducedMotion();
if (reduced) document.documentElement.classList.add("reduced-motion");
window.addEventListener("motion-preference", (e) => {
document.documentElement.classList.toggle("reduced-motion", e.detail.reduced);
ScrollTrigger.refresh();
});
const cards = gsap.utils.toArray(".stack-card");
const total = cards.length;
// Initial stacked position: all centered, slight y offsets, increasing scale-down
cards.forEach((card, i) => {
gsap.set(card, {
y: i * -8,
scale: 1 - (total - 1 - i) * 0.04,
rotateX: 0,
zIndex: i,
opacity: i === total - 1 ? 1 : 0.6,
});
});
if (!reduced) {
const tl = gsap.timeline({
scrollTrigger: {
trigger: ".stack-section",
start: "top top",
end: "bottom bottom",
scrub: 1,
},
});
// Fan out: each card moves to its final position
cards.forEach((card, i) => {
const angle = (i - (total - 1) / 2) * 8; // spread angle
const xOff = (i - (total - 1) / 2) * 120; // horizontal spread
const yOff = Math.abs(i - (total - 1) / 2) * 30; // arc shape
const rot = (i - (total - 1) / 2) * 4; // slight rotation
tl.to(
card,
{
x: xOff,
y: yOff,
rotateY: angle,
rotateZ: rot,
scale: 1,
opacity: 1,
zIndex: i,
duration: 1,
ease: "none",
},
0
);
});
// Second phase: cards settle into a grid-like arrangement
cards.forEach((card, i) => {
const col = i % 3;
const row = Math.floor(i / 3);
const gridX = (col - 1) * 200;
const gridY = row * 240 - 120;
tl.to(
card,
{
x: gridX,
y: gridY,
rotateY: 0,
rotateZ: 0,
rotateX: 0,
scale: 0.85,
duration: 1,
ease: "none",
},
1
);
});
}
// Intro animations
if (!reduced) {
gsap.set(".intro .eyebrow", { opacity: 0, y: 20 });
gsap.set(".intro h1", { opacity: 0, y: 40 });
gsap.set(".intro .subtitle", { opacity: 0, y: 25 });
gsap
.timeline({ defaults: { ease: "expo.out" } })
.to(".intro .eyebrow", { opacity: 1, y: 0, duration: 0.7, delay: 0.3 })
.to(".intro h1", { opacity: 1, y: 0, duration: 0.9 }, "-=0.4")
.to(".intro .subtitle", { opacity: 1, y: 0, duration: 0.7 }, "-=0.5");
}<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Card Stack Cascade — stealthisdesign</title>
<link rel="stylesheet" href="style.css">
<script type="importmap">{"imports":{"gsap":"https://esm.sh/gsap@3.13.0","gsap/ScrollTrigger":"https://esm.sh/gsap@3.13.0/ScrollTrigger","gsap/SplitText":"https://esm.sh/gsap@3.13.0/SplitText","gsap/Flip":"https://esm.sh/gsap@3.13.0/Flip","gsap/ScrambleTextPlugin":"https://esm.sh/gsap@3.13.0/ScrambleTextPlugin","gsap/TextPlugin":"https://esm.sh/gsap@3.13.0/TextPlugin","gsap/all":"https://esm.sh/gsap@3.13.0/all","gsap/":"https://esm.sh/gsap@3.13.0/","lenis":"https://esm.sh/lenis@1.1.13/dist/lenis.mjs","three":"https://esm.sh/three@0.171.0","three/addons/":"https://esm.sh/three@0.171.0/examples/jsm/"}}</script>
<style>html.lenis,
html.lenis body {
height: auto;
}
.lenis:not(.lenis-autoToggle).lenis-stopped {
overflow: clip;
}
.lenis [data-lenis-prevent],
.lenis [data-lenis-prevent-wheel],
.lenis [data-lenis-prevent-touch] {
overscroll-behavior: contain;
}
.lenis.lenis-smooth iframe {
pointer-events: none;
}
.lenis.lenis-autoToggle {
transition-property: overflow;
transition-duration: 1ms;
transition-behavior: allow-discrete;
}</style>
</head>
<body>
<section class="intro">
<span class="eyebrow">Demo 05</span>
<h1>Card Stack Cascade</h1>
<p class="subtitle">Cards start stacked and fan out with 3D transforms as you scroll. Glassmorphism panels with perspective depth.</p>
</section>
<section class="stack-section">
<div class="perspective-container">
<div class="card-stack" id="card-stack">
<div class="stack-card" data-index="0" style="--hue: 200;">
<div class="card-glass">
<span class="card-num">01</span>
<h3>Scroll Animations</h3>
<p>Choreograph complex timelines driven by scroll position with GSAP ScrollTrigger.</p>
<div class="card-tags"><span>gsap</span><span>scrolltrigger</span></div>
</div>
</div>
<div class="stack-card" data-index="1" style="--hue: 260;">
<div class="card-glass">
<span class="card-num">02</span>
<h3>3D WebGL Scenes</h3>
<p>Build immersive particle tunnels, shader backgrounds, and product showcases with Three.js.</p>
<div class="card-tags"><span>three.js</span><span>webgl</span></div>
</div>
</div>
<div class="stack-card" data-index="2" style="--hue: 320;">
<div class="card-glass">
<span class="card-num">03</span>
<h3>View Transitions</h3>
<p>Seamless page transitions with shared-element morphing using the View Transitions API.</p>
<div class="card-tags"><span>view-transitions</span><span>css</span></div>
</div>
</div>
<div class="stack-card" data-index="3" style="--hue: 40;">
<div class="card-glass">
<span class="card-num">04</span>
<h3>Canvas Effects</h3>
<p>Bokeh particles, spotlights, noise overlays, and magnetic cursors with Canvas 2D.</p>
<div class="card-tags"><span>canvas</span><span>vanilla-js</span></div>
</div>
</div>
<div class="stack-card" data-index="4" style="--hue: 160;">
<div class="card-glass">
<span class="card-num">05</span>
<h3>Motion Design</h3>
<p>Shared tokens, easing systems, and accessibility-first motion primitives.</p>
<div class="card-tags"><span>design-system</span><span>a11y</span></div>
</div>
</div>
</div>
</div>
</section>
<section class="outro">
<h2>Stack complete.</h2>
<a href="/" class="btn-back">Back to Showcase</a>
</section>
<script type="module" src="script.js"></script>
</body>
</html>Card Stack Cascade
Stacked cards fan out and rearrange into a grid as you scroll, driven by GSAP ScrollTrigger scrub.
Source
- Repository:
libs-genclaude - Original demo id:
05-card-stack
Notes
Stacked cards fan out and rearrange into a grid as you scroll, driven by GSAP ScrollTrigger scrub.