Three.js Scroll Fold Cards — GSAP Staggered 3D Card Deal
Three.js Scroll Fold Cards · Scroll · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<section class="fld-top"><p>Scroll ↓ to deal the cards</p></section>
<section class="fld-stage" id="fldStage">
<canvas id="fldCanvas"></canvas>
</section>
<section class="fld-bottom"><p>All cards face up.</p></section>*{box-sizing:border-box;margin:0;padding:0}
html,body{background:#0a0812;color:#fff;font-family:system-ui,-apple-system,sans-serif}
.fld-top,.fld-bottom{min-height:70vh;display:flex;justify-content:center;align-items:center;color:#7c749c;font-size:15px;letter-spacing:.08em;text-transform:uppercase}
.fld-stage{height:100vh;position:relative;overflow:hidden;background:radial-gradient(70% 65% at 50% 42%,#171130,#0a0812)}
#fldCanvas{display:block;width:100%;height:100%}const canvas = document.getElementById('fldCanvas');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(50, 1, 0.1, 100);
camera.position.set(0, 0, 12);
camera.lookAt(0, 0, 0);
scene.add(new THREE.AmbientLight(0xffffff, 0.75));
const key = new THREE.DirectionalLight(0xffffff, 1.0); key.position.set(2, 5, 8); scene.add(key);
// A fan of cards that starts collapsed in a single stacked pile at screen left
// (rotated edge-on, invisible) and, as the visitor scrolls, each card flips
// face-up and slides to its place in an even row — a "dealing" reveal.
const CARDS = 7;
const colors = [0x60a5fa, 0xf472b6, 0xfbbf24, 0x34d399, 0xa78bfa, 0xf87171, 0x22d3ee];
const cards = [];
for (let i = 0; i < CARDS; i++) {
// Rounded-ish card via a thin box; front face colored, back face dark.
const card = new THREE.Mesh(
new THREE.BoxGeometry(2, 3, 0.08),
[
new THREE.MeshStandardMaterial({ color: 0x1e1b33, roughness: 0.7 }),
new THREE.MeshStandardMaterial({ color: 0x1e1b33, roughness: 0.7 }),
new THREE.MeshStandardMaterial({ color: 0x1e1b33, roughness: 0.7 }),
new THREE.MeshStandardMaterial({ color: 0x1e1b33, roughness: 0.7 }),
new THREE.MeshStandardMaterial({ color: colors[i], roughness: 0.4, metalness: 0.2, emissive: colors[i], emissiveIntensity: 0.15 }),
new THREE.MeshStandardMaterial({ color: 0x141024, roughness: 0.8 }),
]
);
const targetX = (i - (CARDS - 1) / 2) * 2.35; // final position in the row
card.userData = { targetX, delay: i / CARDS }; // staggered deal order
scene.add(card);
cards.push(card);
}
gsap.registerPlugin(ScrollTrigger);
const state = { p: 0 };
gsap.to(state, {
p: 1, ease: 'none',
scrollTrigger: {
trigger: '#fldStage', start: 'top top', end: '+=450%', scrub: 0.6, pin: true,
},
});
function smooth(t) { t = Math.max(0, Math.min(1, t)); return t * t * (3 - 2 * t); }
function resize() {
const w = canvas.clientWidth, h = canvas.clientHeight;
renderer.setSize(w, h, false);
camera.aspect = w / h;
camera.updateProjectionMatrix();
}
function animate() {
requestAnimationFrame(animate);
cards.forEach((card) => {
const u = card.userData;
// Each card gets its own slice of the scroll: it deals over the window
// [delay, delay + span]. Earlier cards finish before later ones start.
const span = 0.55;
const local = smooth((state.p - u.delay * (1 - span)) / span);
// Rotate from edge-on (PI/2, hidden) to face-up (0), slide from the pile
// at left to the target X, and rise from below into place.
card.rotation.y = (1 - local) * (Math.PI / 2);
card.position.x = (1 - local) * -7 + local * u.targetX;
card.position.y = (1 - local) * -3;
card.position.z = (1 - local) * -2;
card.material.forEach(m => { m.opacity = local; m.transparent = local < 1; });
});
renderer.render(scene, camera);
}
resize();
window.addEventListener('resize', resize);
animate();Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.
What's included
Features
About this UI Snippet
How to Build a Staggered 3D Card Deal on Scroll With Three.js and GSAP

The Three.js Scroll Fold Cards snippet starts with a hidden stack of cards and deals them out one after another as the visitor scrolls — each card flips from edge-on to face-up and slides into its place in an even row — using core Three.js and GSAP's ScrollTrigger plugin, both loaded from a CDN. It's a scroll-driven take on the "cards dealing into place" reveal used across product and pricing pages.
Per-card scroll windows create the stagger
The signature of this effect is that the cards don't all move together — they deal in sequence. Rather than seven separate tweens, the snippet gives each card a delay based on its index and computes a *local* progress: as the global scroll value p advances, each card only animates during its own slice of the scroll, [delay, delay + span]. Earlier cards finish dealing before later ones begin, producing a clean cascade from a single scrubbed value with no timeline orchestration.
Overlapping windows, not gaps
The window span is deliberately larger than the spacing between delays, so the cards' animation windows overlap. This is what makes the deal feel fluid rather than robotic — a card is already sliding in while the previous one is still settling, exactly like a real dealer's rhythm. Widening the span makes the cascade smoother and more simultaneous; narrowing it makes each card snap in more distinctly one at a time.
Three properties animate together per card
Within its window, each card does three things at once, all driven by its smooth-stepped local progress: it rotates from a hidden edge-on π/2 to face-up 0, slides from the collapsed pile at screen-left to its target X in the row, and rises from below into place. Because all three interpolate from the same local value, the flip, the slide, and the rise resolve together into a single coherent motion instead of three disconnected moves.
Materials and face colors sell the flip
Each card is a thin box with a six-material array, so only the front face carries the bright color and a slight emissive glow while the back and edges stay dark. As the card rotates from edge-on to face-up, its colored front swings into view — a genuine 3D flip, not a fake scale trick. The per-material opacity is tied to local progress so cards fade in cleanly as they begin their deal rather than popping from nothing.
Smooth-step easing per card
Each card's local progress runs through the t*t*(3-2t) smooth-step, so it eases into and out of its deal rather than moving linearly. Combined with the overlapping windows, this gives the whole cascade a soft, professional rhythm where cards accelerate off the pile and decelerate into their slot.
scrub: 0.6, pinned, fully reversible
A numeric scrub smooths the deal against noisy input, and because every card's motion derives from the one scrubbed value, scrolling back up gathers all the cards back into the hidden pile in reverse order. This staggered-window technique also appears in the stagger grid ripple snippet; here it choreographs 3D cards. Pair it with a product stages tour or a horizontal gallery.
Step by step
How to Use
- 1Load all three CDN scriptsAdd three.min.js, gsap.min.js, and ScrollTrigger.min.js from the CDN panel, in that order.
- 2Paste HTML, CSS, and JSA hidden stack of cards sits in a pinned 3D stage, ready to deal.
- 3Scroll downCards flip face-up and slide into an even row one after another in a staggered cascade.
- 4Scroll back upThe cards gather back into the hidden pile in reverse, since every card derives from one scrubbed value.
- 5Tune the staggerAdjust the span to make the deal more overlapping and fluid or more distinctly one-at-a-time.
- 6Restyle the cardsChange the CARDS count, the front-face colors array, and the row spacing to fit your content.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Each card has a delay based on its index and computes a local progress from the global scroll value p: it only animates during its own window [delay, delay + span]. As p advances, earlier cards finish before later ones start, so a clean cascade emerges from a single scrubbed value with no per-card timeline to orchestrate.
The span is larger than the spacing between delays, so a card starts dealing while the previous one is still settling. This overlap gives the deal a fluid, dealer-like rhythm. Widening the span makes the cascade smoother and more simultaneous; narrowing it makes each card snap in more distinctly one at a time.
Each card is a thin box with a six-material array, so only the front face carries the bright color and emissive glow while the back and edges are dark. As the card rotates from edge-on (π/2) to face-up (0), the colored front genuinely swings into view — a real rotation, not a scale or opacity fake.
Yes. Adjust the CARDS count, the front-face colors array, and the row spacing. Because each card derives its delay from its index and its target X from its position, the stagger timing and layout recompute automatically — no changes to the animation loop are needed.
Yes. Click JSX for a React component, Vue for a Vue 3 SFC, Angular for a standalone component, or Tailwind for a React + Tailwind version. Build the cards and GSAP timeline inside a mount effect against a canvas ref, drive card data from props, and on cleanup kill the ScrollTrigger and call renderer.dispose() so the pin and WebGL context are released on unmount.