All components

Ashfall Sticky Cards Scroll

GSAP animation component · Published 2026-07-27 · by vanguardia.dev

Open live demo ↗ Raw prompt (.md)

What it does

Five image cards stacked in a centered container are revealed one at a time while a Lenis-driven, ScrollTrigger-pinned section (4 viewport heights) is scrubbed: each front card scales down to 0.5 and rotates 10deg while its image zooms to 1.5, and the next card slides up from y:100% to y:0% to take its place.

How it's built

Categorycards
Techgsap, lenis
GSAP pluginsScrollTrigger
Complexitysection
Performance costlight
Mobile-safeyes
Scrollhijacks scrolling

sticky stacked-cards scroll pin scrub scale rotate lenis

Rebuild it with AI

To reproduce this animation in your own project, copy the prompt below into Claude Code, Cursor or any AI coding agent. The prompt is validated — it describes the exact structure, timing and easing, so the agent rebuilds the effect faithfully and you can then adapt colors, copy and layout to your design.

The full prompt

Sticky Stacked Cards Scroll Reveal (GSAP + Lenis)

Goal

Build a scroll-driven "sticky stacked cards" section: five full-size image cards sit stacked in a centered rounded container inside a pinned section. As the user scrolls, each front card shrinks and rotates away while its image zooms in, and the next card slides up from below to cover it — one card swap per viewport-height of scroll, fully scrubbed to the scrollbar.

Tech

Vanilla HTML/CSS/JS with ES module imports. Use gsap (npm) plus the GSAP plugin ScrollTrigger, and lenis (npm) for smooth scrolling. Register ScrollTrigger with gsap.registerPlugin(ScrollTrigger). Run everything inside a DOMContentLoaded listener.

Layout / HTML

Three full-viewport <section> elements, in order:

  1. <section class="intro"> — a single <h1> with a long editorial sentence about art and motion, e.g. "Art is not what you see. It's what you *feel* in the blur, the chaos, the motion — every pulse captured in color and form."
  2. <section class="sticky-cards"> — contains one <div class="cards-container"> holding exactly 5 <div class="card"> elements. Each card contains:
  3. <div class="tag"><p>LABEL</p></div> — short uppercase labels, one per card, in this order: "Raw Emotion", "Inner Conflict", "Fury & Flow", "Rebellion", "Liberation".
  4. <img src="..."> — one image per card (see Assets).
  5. <section class="outro"> — another <h1> with a closing sentence, e.g. "This isn't just motion. It's meaning in movement. In every blurred edge and amplified hue, we trace the shape of something deeper — truth in abstraction."

Load styles.css via <link> and the script via <script type="module" src="./script.js"> at the end of <body>.

Styling

  • Global reset: * { margin: 0; padding: 0; box-sizing: border-box; }.
  • Fonts (Google Fonts @import in the CSS): body uses "DM Sans"; tag labels use "IBM Plex Mono".
  • img: position: relative; width: 100%; height: 100%; object-fit: cover;.
  • h1: font-size: 5vw; font-weight: 500; line-height: 1; letter-spacing: -0.02em; text-indent: 5em; color: #e3e3db; with antialiased font smoothing.
  • Every section: position: relative; width: 100vw; height: 100svh; padding: 2em; background-color: #1f1f1f; overflow: hidden;.
  • .intro, .outro: flex, centered both axes.
  • .sticky-cards: flex, centered both axes, background-color: #0f0f0f; color: #fff; (darker than the other sections).
  • .cards-container: position: relative; width: 50%; height: 50%; border-radius: 0.5em; overflow: hidden;. The overflow: hidden here is essential — it clips the incoming card that waits below at y: 100%.
  • .card: position: absolute; width: 100%; height: 100%; border-radius: 0.5em; overflow: hidden; — all 5 cards occupy the exact same box, stacked by DOM order (later cards naturally paint on top once they slide in).
  • .tag: position: absolute; top: 1em; left: 1em; padding: 0.5em; border-radius: 0.25em; background: #000; z-index: 1;.
  • .tag p: text-transform: uppercase; font-family: "IBM Plex Mono"; font-size: 12px; font-weight: 600; line-height: 1;.
  • Media query max-width: 1000px: h1 { font-size: 7vw; text-indent: 2em; } and .cards-container { width: 95%; }.

GSAP effect (be exact)

Lenis smooth scroll wiring
const lenis = new Lenis();
lenis.on("scroll", ScrollTrigger.update);
gsap.ticker.add((time) => lenis.raf(time * 1000));
gsap.ticker.lagSmoothing(0);
Initial state (gsap.set)
  • Card index 0 (front card): { y: "0%", scale: 1, rotation: 0 }; its img: { scale: 1 }.
  • Every other card (indices 1–4): { y: "100%", scale: 1, rotation: 0 } (parked one full container-height below, hidden by the container's overflow: hidden); each of their imgs: { scale: 1 }.
Pinned, scrubbed timeline

One gsap.timeline whose ScrollTrigger is:

  • trigger: ".sticky-cards"
  • start: "top top"
  • end: "+=" + window.innerHeight * (totalCards - 1) — i.e. +=4 × window.innerHeight for 5 cards.
  • pin: true (default pinSpacing, so the page gains 4 extra viewport heights of scroll)
  • scrub: 0.5
Timeline content

Loop for (let i = 0; i < totalCards - 1; i++) — 4 transitions. For each i, add three tweens at the same absolute timeline position i (position parameter = the integer i, so transition 0 occupies timeline time 0→1, transition 1 occupies 1→2, etc.). All tweens use duration: 1 and ease: "none" (linear, since the motion is scrubbed):

  1. Current card (cards[i]) → { scale: 0.5, rotation: 10, duration: 1, ease: "none" } — it shrinks to half size and tilts 10 degrees clockwise while still visible behind the incoming card.
  2. Current card's image (images[i]) → { scale: 1.5, duration: 1, ease: "none" } — the photo zooms inside the shrinking card, a counter-zoom parallax.
  3. Next card (cards[i + 1]) → { y: "0%", duration: 1, ease: "none" } — it slides up from 100% to fully cover the container, wiping over the shrinking card beneath it.

All three motions are perfectly simultaneous per transition. Because positions are integers and durations are 1, the four transitions run back-to-back with no gaps or overlaps, and each one maps to exactly one viewport-height of scroll. The last card (index 4) never shrinks — it stays full size when the pin releases.

Assets / images

5 images, one per card, filling the card completely (object-fit: cover; the container is a wide landscape box — roughly 50vw × 50svh). Use abstract expressionist / painterly artworks with blurred, energetic brushstrokes matching each tag's mood:

  1. Warm ochre/red abstract painting with sweeping blurred strokes (Raw Emotion).
  2. Swirling smeared color, chaotic motion blur (Inner Conflict).
  3. High-energy streaked strokes with amplified hues (Fury & Flow).
  4. Bold gestural marks, rebellious blurred motion (Rebellion).
  5. Lighter, freer flowing brushwork suggesting release (Liberation).

Behavior notes

  • The entire effect is scrub-driven: no autonomous animation on load; scrolling backwards reverses everything.
  • Section is pinned for 4 extra viewport heights; intro and outro scroll normally before/after.
  • Works at any viewport size; below 1000px the container widens to 95% and headings scale up.