body { margin: 0; min-height: 100vh; display: grid; place-content: center; background: #060a12; color: #f2f7ff; font-family: "Avenir Next", sans-serif; }
.back { position: fixed; top: 14px; left: 14px; color: #86e8ff; text-decoration: none; }
h1 { text-align: center; margin: 0 0 1rem; }
.stack { position: relative; width: min(88vw, 520px); height: 340px; }
.card { position: absolute; inset: 0; border-radius: 18px; display: grid; place-content: center; font-size: 2rem; font-weight: 800; border: 1px solid #344865; background: linear-gradient(150deg, #223652, #6f3eb2); }
.card:nth-child(2){ background: linear-gradient(150deg, #2a4454, #2e6cb3); }
.card:nth-child(3){ background: linear-gradient(150deg, #4d2f69, #6c3eb2); }
.card:nth-child(4){ background: linear-gradient(150deg, #3f4b7d, #486bb0); }
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;
}
const reduced = window.MotionPreference.prefersReducedMotion();
const cards = Array.from(document.querySelectorAll(".card"));
cards.forEach((c, i) => {
c.style.transform = `translateY(${i * 10}px) scale(${1 - i * 0.03})`;
c.style.zIndex = String(cards.length - i);
});
if (!reduced && window.gsap) {
const tl = window.gsap.timeline({ repeat: -1, repeatDelay: 0.6 });
cards.forEach((c, i) => {
tl.to(c, { y: -220, rotation: i % 2 ? 9 : -9, opacity: 0, duration: 0.45, ease: "power2.in" }, i * 0.22)
.set(c, { y: 0, rotation: 0, opacity: 1 }, i * 0.22 + 0.46);
});
}
<!doctype html>
<html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Demo 07 - Card Stack Choreo</title><link rel="stylesheet" href="style.css" /></head>
<body>
<a class="back" href="../">Back</a>
<main>
<h1>Card Stack Choreography</h1>
<div class="stack" id="stack">
<article class="card">Card 01</article>
<article class="card">Card 02</article>
<article class="card">Card 03</article>
<article class="card">Card 04</article>
</div>
</main>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>
<script src="script.js"></script>
</body></html>