Sticky Cards that Fold Away in 3D on Scroll
Goal
Build a scroll-driven "sticky card deck" page: four full-height, brightly colored art cards stack on top of each other. As you scroll, each card stays pinned while its inner panel folds away in 3D — sliding up, pushing back in Z and tilting 45° on the X axis — and darkens under a fading black overlay as the next card slides over it. Smooth scrolling via Lenis.
Tech
Vanilla HTML/CSS/JS with ES module imports. Use gsap (npm), the GSAP plugin ScrollTrigger, and lenis for smooth scrolling.
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import Lenis from "lenis";
gsap.registerPlugin(ScrollTrigger);
Layout / HTML
Three sections in order:
section.hero— a centered<h1>reading "Art That Lives Online".section.sticky-cards— contains fourdiv.cardelements with idscard-1…card-4. Each card contains onediv.card-innerwith, in this order:div.card-info > p— a short one-line tagline.div.card-title > h1— a huge one-word title.div.card-description > p— a 2–3 sentence description.div.card-img > img— the artwork image.section.outro— a centered<h1>reading "Next Canvas Awaits".
Card copy (title / tagline / description theme):
- Reverie — "A surreal dive into neon hues and playful decay" — a psychedelic skull study in bold candy tones and liquid vector forms, made for covers and prints.
- Vaporwave — "A retro-futurist scene where nostalgia meets glitch" — an 80s-UI dreamscape of stacked windows, checkerboard floors and sunset gradients, like a loading screen to another world.
- Kaleido — "A kaleidoscope of folk motifs reimagined in digital form" — ornamental folk symmetry with stained-glass glow, designed as a seamless tileable pattern.
- Menagerie — "A portrait framed by oddball creatures and doodles" — a playful portrait surrounded by mascots, monsters and midnight snacks; loose linework meets pastel whimsy.
Styling
Fonts (Google Fonts): Barlow Condensed (all weights, notably 900) for all h1s, Host Grotesk as the body font.
Global:
* { margin: 0; padding: 0; box-sizing: border-box; }img { width: 100%; height: 100%; object-fit: cover; }h1 { text-transform: uppercase; font-family: "Barlow Condensed"; font-size: 5rem; font-weight: 900; line-height: 1; }p { text-transform: uppercase; font-weight: 500; }
Palette (CSS variables on :root):
--accent-1: #b1c0ef(light periwinkle blue) → card 1 background--accent-2: #f2acac(soft pink) → card 2--accent-3: #fedd93(pale yellow) → card 3--accent-4: #81b7bf(muted teal) → card 4
Sections:
.hero, .outro:position: relative; width: 100vw; height: 100svh; padding: 2rem;flex centered,text-align: center, background#f9f4eb(warm cream), color#141414..sticky-cards:position: relative; width: 100vw; background-color: #0f0f0f;(near-black — visible behind the cards while they fold).
Cards (this is what makes the 3D fold work — do not skip any of it):
.card:position: sticky; width: 100%; height: 125svh; transform-style: preserve-3d; perspective: 1000px;.card-inner:position: relative; width: 100%; height: 100%; display: flex; flex-direction: column; transform-origin: 50% 100%; will-change: transform; text-align: center;Each card's inner gets its accent variable asbackground-color(#card-1 .card-inner { background-color: var(--accent-1); }, etc.)..card-info:width: 25%; padding: 4em; text-align: left;withp { font-size: 0.9rem; }.card-title h1:font-size: 10rem; padding: 2rem 0;.card-description:width: 60%; margin: 0 auto 2em auto;withp { font-size: 1.5rem; }.card-img:width: 100%; height: 100%; margin-top: 4em; overflow: hidden;(it fills all remaining vertical space; the image inside covers it).
Darkening overlay — drive it with a CSS custom property so GSAP can animate it:
.card-inner::after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #000;
opacity: var(--after-opacity, 0);
will-change: opacity;
pointer-events: none;
z-index: 2;
}
Mobile (@media (max-width: 1000px)): h1 and .card-description become width: calc(100% - 4rem); font-size: 3rem; margin: 0 auto;; .card-info becomes width: 75%; margin: 0 auto; padding: 4em 2em; text-align: center;; .card-title h1 drops to 3rem; .card-description p to 1.25rem.
GSAP effect (the important part — be exact)
Everything runs inside a DOMContentLoaded handler.
1. Lenis ↔ ScrollTrigger sync
const lenis = new Lenis();
lenis.on("scroll", ScrollTrigger.update);
gsap.ticker.add((time) => {
lenis.raf(time * 1000);
});
gsap.ticker.lagSmoothing(0);
2. Per-card fold animation
Collect the cards with gsap.utils.toArray(".card"). Loop over them with their index. Only cards that have a card after them animate (i.e. skip the last card — it never folds; guard with if (index < cards.length - 1)). For each animating card, grab its .card-inner and create TWO scrubbed tweens, both triggered by the NEXT card (cards[index + 1]):
Tween A — the 3D fold (gsap.fromTo on the card's .card-inner):
- From:
{ y: "0%", z: 0, rotationX: 0 } - To:
{ y: "-50%", z: -250, rotationX: 45 } - ScrollTrigger config:
trigger: cards[index + 1](the following card)start: "top 85%"end: "top -75%"scrub: truepin: card— pin the CURRENT card while its successor scrolls over itpinSpacing: false— critical: no spacer is added, so the next card overlaps and covers the pinned one
No ease/duration matter here (fully scrub-driven). Because .card has perspective: 1000px and .card-inner has transform-origin: 50% 100% (bottom-center), the panel visually tips backwards from its bottom edge while receding, like a card falling away into the page.
Tween B — the darkening overlay (gsap.to on the same .card-inner):
- Animates the CSS custom property
"--after-opacity"from its default0to1(this drives the::afterblack layer's opacity). - ScrollTrigger config:
trigger: cards[index + 1]start: "top 75%"end: "top -25%"scrub: true- (no pin on this one)
Note the deliberately offset ranges: the fold spans a longer scroll distance (85% → -75%) than the darkening (75% → -25%), so the shadow ramps up faster and finishes while the card is still folding, selling the "next card casts a shadow over the previous one" illusion.
Assets / images
4 images, one per card, used as full-bleed fillers of the card's lower region (.card-img takes the remaining ~half of a 125svh card at full width, image cropped with object-fit: cover — landscape/wide crops work best, roughly 16:9 or wider). Described by role:
- Card 1 (periwinkle, "Reverie"): psychedelic pop-art vector illustration — a stylized skull with candy-colored accents amid liquid shapes on a deep saturated background.
- Card 2 (pink, "Vaporwave"): retro-futurist vaporwave scene — stacked 80s OS windows, a striped sunset, black-and-white checkerboard floor, pastel gradient shapes.
- Card 3 (yellow, "Kaleido"): symmetrical kaleidoscopic mandala of folk motifs in warm oranges, browns and pinks, centered on an eye surrounded by flowers and hearts.
- Card 4 (teal, "Menagerie"): busy doodle-art portrait of a stylized figure surrounded by cartoon mascots, monsters and small objects in loose pastel linework.
Behavior notes
- The effect is purely scroll-scrubbed — scrolling back up reverses the fold perfectly.
- The last card never animates or pins; it simply gets covered by the outro section scrolling over the stack's flow.
- Cards are 125svh tall on purpose (taller than the viewport) so consecutive cards overlap generously during the transition.
- Works on mobile (the media query only adjusts typography/widths; the fold effect is unchanged).
- No load-in animation: the page is static until the user scrolls.