29 / 36
All 36 in 36 Tailwind Stacked Cards
- 01Tailwind Scroll-Driven Scaling Card Stack
- 02Tailwind Swipe Card Deck Drag to Dismiss
- 03Tailwind Glassmorphism Testimonial Card Stack
- 04Tailwind Stacked Cards Expand Accordion on Click
- 05Tailwind Fan Out Photo Stack on Hover
- 06Tailwind 3D Perspective Tilt Card Stack
- 07Tailwind Multi Step Form Card Stack
- 08Tailwind Tabbed Card Stack Bring to Front
- 09Tailwind Click to Front Stacked Cards Cycle
- 10Tailwind Classic Card Deck Stack Shadow
- 11Tailwind Fan Spread Hand of Cards
- 12Tailwind Polaroid Photo Stack Scattered Rotate
- 13Tailwind Staircase Offset Stacked Cards Cascade
- 14Tailwind Cards Spread Out Horizontally on Hover
- 15Tailwind Stacked Cards Lift on Hover Sibling Push
- 16Tailwind Accordion Stacked Cards Slide Out Menu
- 17Tailwind Shuffle Cards Animation Loop Deck
- 18Tailwind File Folder Tab Stacked Cards
- 19Tailwind Scattered Photos Pinch to Centre on Hover
- 20Tailwind Mobile Swipe Card Stack Translate3d
- 21Tailwind Single Accordion Card Nested Layers
- 22Tailwind Isometric Tower Stacked Cards 3D Column
- 23Tailwind 3D Flip Card Stack Reveal Back
- 24Tailwind Card Stack Tilt on Hover Quadrant
- 25Tailwind Notification Stack Pile Dismiss
- 26Tailwind Receipt Roll Unroll Stacked Items
- 27Tailwind Scratch Card Reveal Canvas Mask
- 28Tailwind Stacked Cards Hover Reveal Spread
- 29Tailwind Scroll Activated Sticky Stacking Cards
- 30Tailwind Stacked Testimonial Slider Radio
- 31Tailwind Isometric Overlapping Stacked Cards
- 32Tailwind Bento Grid Stacked Cards Hover Fan
- 33Tailwind Perspective Card Deck Depth Tunnel
- 34Tailwind Peel Back Corner Card Reveal
- 35Tailwind Timeline Stacked Milestone Cards
- 36Tailwind Spiral Tower Rotating Stacked Cards
Tailwind Scroll Activated Sticky Stacking Cards
The How-It-Works sequence: each step pins, and the one behind it scales down and dims as the next slides over. An IntersectionObserver sets one data-state per card — ahead, live, behind — and every visual difference is a data-[state=…] variant, which is both cheaper than scroll maths and far easier to read.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="stk-29 relative w-full min-h-svh bg-stk29-void font-stk29-sans text-white">
<div class="pointer-events-none fixed -inset-[30%] z-0 blur-[30px] animate-stk29-drift motion-reduce:animate-none [background-image:radial-gradient(40%_40%_at_25%_25%,var(--color-stk29-aur1),transparent_70%),radial-gradient(45%_45%_at_75%_30%,var(--color-stk29-aur2),transparent_70%),radial-gradient(50%_50%_at_50%_80%,var(--color-stk29-aur3),transparent_70%)]" aria-hidden="true"></div>
<div class="pointer-events-none fixed inset-0 z-0 mix-blend-overlay [background-image:repeating-linear-gradient(0deg,rgba(255,255,255,0.02)_0_1px,transparent_1px_3px)]" aria-hidden="true"></div>
<div class="fixed right-6 top-1/2 z-5 flex -translate-y-1/2 flex-col gap-3.5" id="stk-29-rail" aria-hidden="true">
<span class="block size-2 rounded-full bg-white/25 transition-all duration-400 data-[on=true]:h-6.5 data-[on=true]:rounded-md data-[on=true]:bg-white data-[on=true]:shadow-[0_0_14px_rgba(255,255,255,0.6)]" data-on="true"></span>
<span class="block size-2 rounded-full bg-white/25 transition-all duration-400 data-[on=true]:h-6.5 data-[on=true]:rounded-md data-[on=true]:bg-white data-[on=true]:shadow-[0_0_14px_rgba(255,255,255,0.6)]"></span>
<span class="block size-2 rounded-full bg-white/25 transition-all duration-400 data-[on=true]:h-6.5 data-[on=true]:rounded-md data-[on=true]:bg-white data-[on=true]:shadow-[0_0_14px_rgba(255,255,255,0.6)]"></span>
<span class="block size-2 rounded-full bg-white/25 transition-all duration-400 data-[on=true]:h-6.5 data-[on=true]:rounded-md data-[on=true]:bg-white data-[on=true]:shadow-[0_0_14px_rgba(255,255,255,0.6)]"></span>
</div>
<p class="sr-only" id="stk-29-live" aria-live="polite"></p>
<div class="relative z-1 mx-auto w-[min(460px,92vw)] pt-[55vh] pb-[75vh]" id="stk-29-steps">
<section class="sticky top-[20vh] mb-9 flex h-80 flex-col justify-between overflow-hidden rounded-[28px] p-9.5 origin-top backdrop-blur-[6px] shadow-[0_40px_80px_rgba(0,0,0,0.55),inset_0_0_0_1px_rgba(255,255,255,0.08)] transition-[transform,filter] duration-500 ease-stk29 data-[state=behind]:scale-[0.94] data-[state=behind]:brightness-[0.7] data-[state=behind]:-translate-y-2 motion-reduce:transition-none motion-reduce:scale-100! motion-reduce:brightness-100!" data-state="live" data-label="Discover">
<div class="absolute inset-0 -z-1 opacity-95 bg-[size:200%_200%] animate-stk29-flow motion-reduce:animate-none bg-linear-[135deg,var(--color-stk29-a1),var(--color-stk29-a2)_45%,var(--color-stk29-a3)]" aria-hidden="true"></div>
<div class="absolute -top-1/2 -left-[30%] h-[200%] w-1/2 animate-stk29-sweep motion-reduce:animate-none [background-image:linear-gradient(90deg,transparent,rgba(255,255,255,0.35),transparent)]" aria-hidden="true"></div>
<div class="flex items-center gap-3.5">
<span class="font-stk29-serif text-[4rem] italic leading-[0.8] opacity-90 [text-shadow:0_4px_20px_rgba(0,0,0,0.3)]">01</span>
<span class="ml-auto rounded-full border border-white/25 bg-white/18 px-3.5 py-1.75 text-[0.62rem] tracking-[0.22em] uppercase backdrop-blur-[6px]">Phase one</span>
</div>
<div>
<h2 class="m-0 font-stk29-serif text-[2.4rem] tracking-[-0.01em]">Discover</h2>
<p class="m-0 mt-1 max-w-[32ch] font-light leading-relaxed opacity-90">We map your goals and audit exactly where you stand today.</p>
<div class="mt-4 h-1 overflow-hidden rounded bg-white/25"><i class="block h-full w-1/4 rounded bg-white transition-[width] duration-500"></i></div>
</div>
</section>
<section class="sticky top-[20vh] mb-9 flex h-80 flex-col justify-between overflow-hidden rounded-[28px] p-9.5 origin-top backdrop-blur-[6px] shadow-[0_40px_80px_rgba(0,0,0,0.55),inset_0_0_0_1px_rgba(255,255,255,0.08)] transition-[transform,filter] duration-500 ease-stk29 data-[state=behind]:scale-[0.94] data-[state=behind]:brightness-[0.7] data-[state=behind]:-translate-y-2 motion-reduce:transition-none motion-reduce:scale-100! motion-reduce:brightness-100!" data-state="ahead" data-label="Design">
<div class="absolute inset-0 -z-1 opacity-95 bg-[size:200%_200%] animate-stk29-flow motion-reduce:animate-none bg-linear-[135deg,var(--color-stk29-b1),var(--color-stk29-b2)_45%,var(--color-stk29-b3)]" aria-hidden="true"></div>
<div class="absolute -top-1/2 -left-[30%] h-[200%] w-1/2 animate-stk29-sweep motion-reduce:animate-none [background-image:linear-gradient(90deg,transparent,rgba(255,255,255,0.35),transparent)]" aria-hidden="true"></div>
<div class="flex items-center gap-3.5">
<span class="font-stk29-serif text-[4rem] italic leading-[0.8] opacity-90 [text-shadow:0_4px_20px_rgba(0,0,0,0.3)]">02</span>
<span class="ml-auto rounded-full border border-white/25 bg-white/18 px-3.5 py-1.75 text-[0.62rem] tracking-[0.22em] uppercase backdrop-blur-[6px]">Phase two</span>
</div>
<div>
<h2 class="m-0 font-stk29-serif text-[2.4rem] tracking-[-0.01em]">Design</h2>
<p class="m-0 mt-1 max-w-[32ch] font-light leading-relaxed opacity-90">Concepts take shape through rapid, collaborative iteration.</p>
<div class="mt-4 h-1 overflow-hidden rounded bg-white/25"><i class="block h-full w-2/4 rounded bg-white transition-[width] duration-500"></i></div>
</div>
</section>
<section class="sticky top-[20vh] mb-9 flex h-80 flex-col justify-between overflow-hidden rounded-[28px] p-9.5 origin-top backdrop-blur-[6px] shadow-[0_40px_80px_rgba(0,0,0,0.55),inset_0_0_0_1px_rgba(255,255,255,0.08)] transition-[transform,filter] duration-500 ease-stk29 data-[state=behind]:scale-[0.94] data-[state=behind]:brightness-[0.7] data-[state=behind]:-translate-y-2 motion-reduce:transition-none motion-reduce:scale-100! motion-reduce:brightness-100!" data-state="ahead" data-label="Build">
<div class="absolute inset-0 -z-1 opacity-95 bg-[size:200%_200%] animate-stk29-flow motion-reduce:animate-none bg-linear-[135deg,var(--color-stk29-c1),var(--color-stk29-c2)_50%,var(--color-stk29-c3)]" aria-hidden="true"></div>
<div class="absolute -top-1/2 -left-[30%] h-[200%] w-1/2 animate-stk29-sweep motion-reduce:animate-none [background-image:linear-gradient(90deg,transparent,rgba(255,255,255,0.35),transparent)]" aria-hidden="true"></div>
<div class="flex items-center gap-3.5">
<span class="font-stk29-serif text-[4rem] italic leading-[0.8] opacity-90 [text-shadow:0_4px_20px_rgba(0,0,0,0.3)]">03</span>
<span class="ml-auto rounded-full border border-white/25 bg-white/18 px-3.5 py-1.75 text-[0.62rem] tracking-[0.22em] uppercase backdrop-blur-[6px]">Phase three</span>
</div>
<div>
<h2 class="m-0 font-stk29-serif text-[2.4rem] tracking-[-0.01em]">Build</h2>
<p class="m-0 mt-1 max-w-[32ch] font-light leading-relaxed opacity-90">Production-ready code, tested and shipped with real care.</p>
<div class="mt-4 h-1 overflow-hidden rounded bg-white/25"><i class="block h-full w-3/4 rounded bg-white transition-[width] duration-500"></i></div>
</div>
</section>
<section class="sticky top-[20vh] mb-9 flex h-80 flex-col justify-between overflow-hidden rounded-[28px] p-9.5 origin-top backdrop-blur-[6px] shadow-[0_40px_80px_rgba(0,0,0,0.55),inset_0_0_0_1px_rgba(255,255,255,0.08)] transition-[transform,filter] duration-500 ease-stk29 data-[state=behind]:scale-[0.94] data-[state=behind]:brightness-[0.7] data-[state=behind]:-translate-y-2 motion-reduce:transition-none motion-reduce:scale-100! motion-reduce:brightness-100!" data-state="ahead" data-label="Launch">
<div class="absolute inset-0 -z-1 opacity-95 bg-[size:200%_200%] animate-stk29-flow motion-reduce:animate-none bg-linear-[135deg,var(--color-stk29-d1),var(--color-stk29-d2)_45%,var(--color-stk29-d3)]" aria-hidden="true"></div>
<div class="absolute -top-1/2 -left-[30%] h-[200%] w-1/2 animate-stk29-sweep motion-reduce:animate-none [background-image:linear-gradient(90deg,transparent,rgba(255,255,255,0.35),transparent)]" aria-hidden="true"></div>
<div class="flex items-center gap-3.5">
<span class="font-stk29-serif text-[4rem] italic leading-[0.8] opacity-90 [text-shadow:0_4px_20px_rgba(0,0,0,0.3)]">04</span>
<span class="ml-auto rounded-full border border-white/25 bg-white/18 px-3.5 py-1.75 text-[0.62rem] tracking-[0.22em] uppercase backdrop-blur-[6px]">Phase four</span>
</div>
<div>
<h2 class="m-0 font-stk29-serif text-[2.4rem] tracking-[-0.01em]">Launch</h2>
<p class="m-0 mt-1 max-w-[32ch] font-light leading-relaxed opacity-90">We go live, measure everything, then refine what works.</p>
<div class="mt-4 h-1 overflow-hidden rounded bg-white/25"><i class="block h-full w-full rounded bg-white transition-[width] duration-500"></i></div>
</div>
</section>
</div>
</section><section class="stk-29 relative w-full min-h-svh bg-stk29-void font-stk29-sans text-white">
<div class="pointer-events-none fixed -inset-[30%] z-0 blur-[30px] animate-stk29-drift motion-reduce:animate-none [background-image:radial-gradient(40%_40%_at_25%_25%,var(--color-stk29-aur1),transparent_70%),radial-gradient(45%_45%_at_75%_30%,var(--color-stk29-aur2),transparent_70%),radial-gradient(50%_50%_at_50%_80%,var(--color-stk29-aur3),transparent_70%)]" aria-hidden="true"></div>
<div class="pointer-events-none fixed inset-0 z-0 mix-blend-overlay [background-image:repeating-linear-gradient(0deg,rgba(255,255,255,0.02)_0_1px,transparent_1px_3px)]" aria-hidden="true"></div>
<div class="fixed right-6 top-1/2 z-5 flex -translate-y-1/2 flex-col gap-3.5" id="stk-29-rail" aria-hidden="true">
<span class="block size-2 rounded-full bg-white/25 transition-all duration-400 data-[on=true]:h-6.5 data-[on=true]:rounded-md data-[on=true]:bg-white data-[on=true]:shadow-[0_0_14px_rgba(255,255,255,0.6)]" data-on="true"></span>
<span class="block size-2 rounded-full bg-white/25 transition-all duration-400 data-[on=true]:h-6.5 data-[on=true]:rounded-md data-[on=true]:bg-white data-[on=true]:shadow-[0_0_14px_rgba(255,255,255,0.6)]"></span>
<span class="block size-2 rounded-full bg-white/25 transition-all duration-400 data-[on=true]:h-6.5 data-[on=true]:rounded-md data-[on=true]:bg-white data-[on=true]:shadow-[0_0_14px_rgba(255,255,255,0.6)]"></span>
<span class="block size-2 rounded-full bg-white/25 transition-all duration-400 data-[on=true]:h-6.5 data-[on=true]:rounded-md data-[on=true]:bg-white data-[on=true]:shadow-[0_0_14px_rgba(255,255,255,0.6)]"></span>
</div>
<p class="sr-only" id="stk-29-live" aria-live="polite"></p>
<div class="relative z-1 mx-auto w-[min(460px,92vw)] pt-[55vh] pb-[75vh]" id="stk-29-steps">
<section class="sticky top-[20vh] mb-9 flex h-80 flex-col justify-between overflow-hidden rounded-[28px] p-9.5 origin-top backdrop-blur-[6px] shadow-[0_40px_80px_rgba(0,0,0,0.55),inset_0_0_0_1px_rgba(255,255,255,0.08)] transition-[transform,filter] duration-500 ease-stk29 data-[state=behind]:scale-[0.94] data-[state=behind]:brightness-[0.7] data-[state=behind]:-translate-y-2 motion-reduce:transition-none motion-reduce:scale-100! motion-reduce:brightness-100!" data-state="live" data-label="Discover">
<div class="absolute inset-0 -z-1 opacity-95 bg-[size:200%_200%] animate-stk29-flow motion-reduce:animate-none bg-linear-[135deg,var(--color-stk29-a1),var(--color-stk29-a2)_45%,var(--color-stk29-a3)]" aria-hidden="true"></div>
<div class="absolute -top-1/2 -left-[30%] h-[200%] w-1/2 animate-stk29-sweep motion-reduce:animate-none [background-image:linear-gradient(90deg,transparent,rgba(255,255,255,0.35),transparent)]" aria-hidden="true"></div>
<div class="flex items-center gap-3.5">
<span class="font-stk29-serif text-[4rem] italic leading-[0.8] opacity-90 [text-shadow:0_4px_20px_rgba(0,0,0,0.3)]">01</span>
<span class="ml-auto rounded-full border border-white/25 bg-white/18 px-3.5 py-1.75 text-[0.62rem] tracking-[0.22em] uppercase backdrop-blur-[6px]">Phase one</span>
</div>
<div>
<h2 class="m-0 font-stk29-serif text-[2.4rem] tracking-[-0.01em]">Discover</h2>
<p class="m-0 mt-1 max-w-[32ch] font-light leading-relaxed opacity-90">We map your goals and audit exactly where you stand today.</p>
<div class="mt-4 h-1 overflow-hidden rounded bg-white/25"><i class="block h-full w-1/4 rounded bg-white transition-[width] duration-500"></i></div>
</div>
</section>
<section class="sticky top-[20vh] mb-9 flex h-80 flex-col justify-between overflow-hidden rounded-[28px] p-9.5 origin-top backdrop-blur-[6px] shadow-[0_40px_80px_rgba(0,0,0,0.55),inset_0_0_0_1px_rgba(255,255,255,0.08)] transition-[transform,filter] duration-500 ease-stk29 data-[state=behind]:scale-[0.94] data-[state=behind]:brightness-[0.7] data-[state=behind]:-translate-y-2 motion-reduce:transition-none motion-reduce:scale-100! motion-reduce:brightness-100!" data-state="ahead" data-label="Design">
<div class="absolute inset-0 -z-1 opacity-95 bg-[size:200%_200%] animate-stk29-flow motion-reduce:animate-none bg-linear-[135deg,var(--color-stk29-b1),var(--color-stk29-b2)_45%,var(--color-stk29-b3)]" aria-hidden="true"></div>
<div class="absolute -top-1/2 -left-[30%] h-[200%] w-1/2 animate-stk29-sweep motion-reduce:animate-none [background-image:linear-gradient(90deg,transparent,rgba(255,255,255,0.35),transparent)]" aria-hidden="true"></div>
<div class="flex items-center gap-3.5">
<span class="font-stk29-serif text-[4rem] italic leading-[0.8] opacity-90 [text-shadow:0_4px_20px_rgba(0,0,0,0.3)]">02</span>
<span class="ml-auto rounded-full border border-white/25 bg-white/18 px-3.5 py-1.75 text-[0.62rem] tracking-[0.22em] uppercase backdrop-blur-[6px]">Phase two</span>
</div>
<div>
<h2 class="m-0 font-stk29-serif text-[2.4rem] tracking-[-0.01em]">Design</h2>
<p class="m-0 mt-1 max-w-[32ch] font-light leading-relaxed opacity-90">Concepts take shape through rapid, collaborative iteration.</p>
<div class="mt-4 h-1 overflow-hidden rounded bg-white/25"><i class="block h-full w-2/4 rounded bg-white transition-[width] duration-500"></i></div>
</div>
</section>
<section class="sticky top-[20vh] mb-9 flex h-80 flex-col justify-between overflow-hidden rounded-[28px] p-9.5 origin-top backdrop-blur-[6px] shadow-[0_40px_80px_rgba(0,0,0,0.55),inset_0_0_0_1px_rgba(255,255,255,0.08)] transition-[transform,filter] duration-500 ease-stk29 data-[state=behind]:scale-[0.94] data-[state=behind]:brightness-[0.7] data-[state=behind]:-translate-y-2 motion-reduce:transition-none motion-reduce:scale-100! motion-reduce:brightness-100!" data-state="ahead" data-label="Build">
<div class="absolute inset-0 -z-1 opacity-95 bg-[size:200%_200%] animate-stk29-flow motion-reduce:animate-none bg-linear-[135deg,var(--color-stk29-c1),var(--color-stk29-c2)_50%,var(--color-stk29-c3)]" aria-hidden="true"></div>
<div class="absolute -top-1/2 -left-[30%] h-[200%] w-1/2 animate-stk29-sweep motion-reduce:animate-none [background-image:linear-gradient(90deg,transparent,rgba(255,255,255,0.35),transparent)]" aria-hidden="true"></div>
<div class="flex items-center gap-3.5">
<span class="font-stk29-serif text-[4rem] italic leading-[0.8] opacity-90 [text-shadow:0_4px_20px_rgba(0,0,0,0.3)]">03</span>
<span class="ml-auto rounded-full border border-white/25 bg-white/18 px-3.5 py-1.75 text-[0.62rem] tracking-[0.22em] uppercase backdrop-blur-[6px]">Phase three</span>
</div>
<div>
<h2 class="m-0 font-stk29-serif text-[2.4rem] tracking-[-0.01em]">Build</h2>
<p class="m-0 mt-1 max-w-[32ch] font-light leading-relaxed opacity-90">Production-ready code, tested and shipped with real care.</p>
<div class="mt-4 h-1 overflow-hidden rounded bg-white/25"><i class="block h-full w-3/4 rounded bg-white transition-[width] duration-500"></i></div>
</div>
</section>
<section class="sticky top-[20vh] mb-9 flex h-80 flex-col justify-between overflow-hidden rounded-[28px] p-9.5 origin-top backdrop-blur-[6px] shadow-[0_40px_80px_rgba(0,0,0,0.55),inset_0_0_0_1px_rgba(255,255,255,0.08)] transition-[transform,filter] duration-500 ease-stk29 data-[state=behind]:scale-[0.94] data-[state=behind]:brightness-[0.7] data-[state=behind]:-translate-y-2 motion-reduce:transition-none motion-reduce:scale-100! motion-reduce:brightness-100!" data-state="ahead" data-label="Launch">
<div class="absolute inset-0 -z-1 opacity-95 bg-[size:200%_200%] animate-stk29-flow motion-reduce:animate-none bg-linear-[135deg,var(--color-stk29-d1),var(--color-stk29-d2)_45%,var(--color-stk29-d3)]" aria-hidden="true"></div>
<div class="absolute -top-1/2 -left-[30%] h-[200%] w-1/2 animate-stk29-sweep motion-reduce:animate-none [background-image:linear-gradient(90deg,transparent,rgba(255,255,255,0.35),transparent)]" aria-hidden="true"></div>
<div class="flex items-center gap-3.5">
<span class="font-stk29-serif text-[4rem] italic leading-[0.8] opacity-90 [text-shadow:0_4px_20px_rgba(0,0,0,0.3)]">04</span>
<span class="ml-auto rounded-full border border-white/25 bg-white/18 px-3.5 py-1.75 text-[0.62rem] tracking-[0.22em] uppercase backdrop-blur-[6px]">Phase four</span>
</div>
<div>
<h2 class="m-0 font-stk29-serif text-[2.4rem] tracking-[-0.01em]">Launch</h2>
<p class="m-0 mt-1 max-w-[32ch] font-light leading-relaxed opacity-90">We go live, measure everything, then refine what works.</p>
<div class="mt-4 h-1 overflow-hidden rounded bg-white/25"><i class="block h-full w-full rounded bg-white transition-[width] duration-500"></i></div>
</div>
</section>
</div>
</section>@import "tailwindcss";
@theme {
--color-stk29-void: #05060a;
--color-stk29-void: oklch(0.08 0.012 275);
--color-stk29-aur1: rgba(99, 102, 241, 0.30);
--color-stk29-aur2: rgba(236, 72, 153, 0.26);
--color-stk29-aur3: rgba(45, 212, 191, 0.24);
--color-stk29-a1: #1e1b4b;
--color-stk29-a2: #4338ca;
--color-stk29-a3: #6d28d9;
--color-stk29-b1: #831843;
--color-stk29-b2: #be185d;
--color-stk29-b3: #f43f5e;
--color-stk29-c1: #064e3b;
--color-stk29-c2: #0d9488;
--color-stk29-c3: #2dd4bf;
--color-stk29-d1: #7c2d12;
--color-stk29-d2: #ea580c;
--color-stk29-d3: #f59e0b;
--font-stk29-sans: 'Geist', system-ui, sans-serif;
--font-stk29-serif: 'Instrument Serif', Georgia, serif;
--ease-stk29: cubic-bezier(0.3, 0.9, 0.3, 1);
--animate-stk29-flow: stk-29-flow 9s ease infinite;
--animate-stk29-drift: stk-29-drift 20s ease-in-out infinite alternate;
--animate-stk29-sweep: stk-29-sweep 6s ease-in-out infinite;
}
@keyframes stk-29-flow {
0%,
100% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
}
@keyframes stk-29-drift {
from {
transform: translate(-3%, -2%) scale(1);
}
to {
transform: translate(4%, 3%) scale(1.1);
}
}
@keyframes stk-29-sweep {
0%,
100% {
transform: rotate(18deg) translateX(-300px);
}
50% {
transform: rotate(18deg) translateX(700px);
}
}@import "tailwindcss";
@theme {
--color-stk29-void: #05060a;
--color-stk29-void: oklch(0.08 0.012 275);
--color-stk29-aur1: rgba(99, 102, 241, 0.30);
--color-stk29-aur2: rgba(236, 72, 153, 0.26);
--color-stk29-aur3: rgba(45, 212, 191, 0.24);
--color-stk29-a1: #1e1b4b;
--color-stk29-a2: #4338ca;
--color-stk29-a3: #6d28d9;
--color-stk29-b1: #831843;
--color-stk29-b2: #be185d;
--color-stk29-b3: #f43f5e;
--color-stk29-c1: #064e3b;
--color-stk29-c2: #0d9488;
--color-stk29-c3: #2dd4bf;
--color-stk29-d1: #7c2d12;
--color-stk29-d2: #ea580c;
--color-stk29-d3: #f59e0b;
--font-stk29-sans: 'Geist', system-ui, sans-serif;
--font-stk29-serif: 'Instrument Serif', Georgia, serif;
--ease-stk29: cubic-bezier(0.3, 0.9, 0.3, 1);
--animate-stk29-flow: stk-29-flow 9s ease infinite;
--animate-stk29-drift: stk-29-drift 20s ease-in-out infinite alternate;
--animate-stk29-sweep: stk-29-sweep 6s ease-in-out infinite;
}
@keyframes stk-29-flow {
0%,
100% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
}
@keyframes stk-29-drift {
from {
transform: translate(-3%, -2%) scale(1);
}
to {
transform: translate(4%, 3%) scale(1.1);
}
}
@keyframes stk-29-sweep {
0%,
100% {
transform: rotate(18deg) translateX(-300px);
}
50% {
transform: rotate(18deg) translateX(700px);
}
}(() => {
const root = document.querySelector('.stk-29');
if (!root) return;
const steps = [...root.querySelectorAll('#stk-29-steps > section')];
const pips = [...root.querySelectorAll('#stk-29-rail > span')];
const live = root.querySelector('#stk-29-live');
let current = -1;
function setCurrent(i) {
if (i === current) return;
current = i;
steps.forEach((s, n) => { s.dataset.state = n < i ? 'behind' : n === i ? 'live' : 'ahead'; });
pips.forEach((p, n) => { if (n === i) p.dataset.on = 'true'; else delete p.dataset.on; });
/* A pin is a visual event and nothing more — say which step it is. */
live.textContent = 'Step ' + (i + 1) + ' of ' + steps.length + ': ' + steps[i].dataset.label;
}
/* An observer, not a scroll listener: nothing here runs per frame and no
layout is read during scrolling. */
const io = new IntersectionObserver((entries) => {
entries.forEach((e) => {
if (e.isIntersecting) setCurrent(steps.indexOf(e.target));
});
}, { rootMargin: '-20% 0px -60% 0px', threshold: 0 });
steps.forEach((s) => io.observe(s));
setCurrent(0);
})();(() => {
const root = document.querySelector('.stk-29');
if (!root) return;
const steps = [...root.querySelectorAll('#stk-29-steps > section')];
const pips = [...root.querySelectorAll('#stk-29-rail > span')];
const live = root.querySelector('#stk-29-live');
let current = -1;
function setCurrent(i) {
if (i === current) return;
current = i;
steps.forEach((s, n) => { s.dataset.state = n < i ? 'behind' : n === i ? 'live' : 'ahead'; });
pips.forEach((p, n) => { if (n === i) p.dataset.on = 'true'; else delete p.dataset.on; });
/* A pin is a visual event and nothing more — say which step it is. */
live.textContent = 'Step ' + (i + 1) + ' of ' + steps.length + ': ' + steps[i].dataset.label;
}
/* An observer, not a scroll listener: nothing here runs per frame and no
layout is read during scrolling. */
const io = new IntersectionObserver((entries) => {
entries.forEach((e) => {
if (e.isIntersecting) setCurrent(steps.indexOf(e.target));
});
}, { rootMargin: '-20% 0px -60% 0px', threshold: 0 });
steps.forEach((s) => io.observe(s));
setCurrent(0);
})();Setup — do this once per project
Skip this if your project already runs Tailwind v4.
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>Compiles in the browser — perfect for a CodePen or a quick test, not for production.
npm install tailwindcss @tailwindcss/vite
npm install tailwindcss @tailwindcss/vite// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})Any v4 setup works — Vite, PostCSS or the CLI. Nothing in this demo is bundler-specific.
@import "tailwindcss";
@theme {
--color-stk29-void: #05060a;
--color-stk29-void: oklch(0.08 0.012 275);
--color-stk29-aur1: rgba(99, 102, 241, 0.30);
--color-stk29-aur2: rgba(236, 72, 153, 0.26);
--color-stk29-aur3: rgba(45, 212, 191, 0.24);
--color-stk29-a1: #1e1b4b;
--color-stk29-a2: #4338ca;
--color-stk29-a3: #6d28d9;
--color-stk29-b1: #831843;
--color-stk29-b2: #be185d;
--color-stk29-b3: #f43f5e;
--color-stk29-c1: #064e3b;
--color-stk29-c2: #0d9488;
--color-stk29-c3: #2dd4bf;
--color-stk29-d1: #7c2d12;
--color-stk29-d2: #ea580c;
--color-stk29-d3: #f59e0b;
--font-stk29-sans: 'Geist', system-ui, sans-serif;
--font-stk29-serif: 'Instrument Serif', Georgia, serif;
--ease-stk29: cubic-bezier(0.3, 0.9, 0.3, 1);
--animate-stk29-flow: stk-29-flow 9s ease infinite;
--animate-stk29-drift: stk-29-drift 20s ease-in-out infinite alternate;
--animate-stk29-sweep: stk-29-sweep 6s ease-in-out infinite;
}
@keyframes stk-29-flow {
0%,
100% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
}
@keyframes stk-29-drift {
from {
transform: translate(-3%, -2%) scale(1);
}
to {
transform: translate(4%, 3%) scale(1.1);
}
}
@keyframes stk-29-sweep {
0%,
100% {
transform: rotate(18deg) translateX(-300px);
}
50% {
transform: rotate(18deg) translateX(700px);
}
}@import "tailwindcss";
@theme {
--color-stk29-void: #05060a;
--color-stk29-void: oklch(0.08 0.012 275);
--color-stk29-aur1: rgba(99, 102, 241, 0.30);
--color-stk29-aur2: rgba(236, 72, 153, 0.26);
--color-stk29-aur3: rgba(45, 212, 191, 0.24);
--color-stk29-a1: #1e1b4b;
--color-stk29-a2: #4338ca;
--color-stk29-a3: #6d28d9;
--color-stk29-b1: #831843;
--color-stk29-b2: #be185d;
--color-stk29-b3: #f43f5e;
--color-stk29-c1: #064e3b;
--color-stk29-c2: #0d9488;
--color-stk29-c3: #2dd4bf;
--color-stk29-d1: #7c2d12;
--color-stk29-d2: #ea580c;
--color-stk29-d3: #f59e0b;
--font-stk29-sans: 'Geist', system-ui, sans-serif;
--font-stk29-serif: 'Instrument Serif', Georgia, serif;
--ease-stk29: cubic-bezier(0.3, 0.9, 0.3, 1);
--animate-stk29-flow: stk-29-flow 9s ease infinite;
--animate-stk29-drift: stk-29-drift 20s ease-in-out infinite alternate;
--animate-stk29-sweep: stk-29-sweep 6s ease-in-out infinite;
}
@keyframes stk-29-flow {
0%,
100% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
}
@keyframes stk-29-drift {
from {
transform: translate(-3%, -2%) scale(1);
}
to {
transform: translate(4%, 3%) scale(1.1);
}
}
@keyframes stk-29-sweep {
0%,
100% {
transform: rotate(18deg) translateX(-300px);
}
50% {
transform: rotate(18deg) translateX(700px);
}
}<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@300;400;500;600;700&display=swap">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@300;400;500;600;700&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- The webfonts load as
<link>tags insetup, not as an@importinside the stylesheet. Tailwind emits user@importrules after its own@layerblocks, and a late@importis invalid CSS — the browser drops it and the demo silently falls back to system fonts. - The scroll handler becomes an
IntersectionObserver, so nothing measures layout during scrolling — the difference is visible directly in INP. - The controller sets one
data-stateper card and the recession lives indata-[state=behind]:variants; the script contains no styles at all. origin-topis the detail that makes it read as sinking rather than deflating — a centre-origin scale looks like the card is shrinking in place.- The pip rail is driven by
data-[on=true]:variants and markedaria-hidden, with the real progress carried by thearia-liveregion beside it. - No
overflowutility appears anywhere in the ancestor chain, becauseposition:stickyfails silently under a clipped ancestor — the commonest reason a sticky stack ‘does not work’. - The reduced-motion path uses
motion-reduce:scale-100!andmotion-reduce:brightness-100!to beat thedata-[state=behind]variants, so the cards just scroll past one another.
Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind Scroll Activated Sticky Stacking Cards
Source: https://codefronts.com/components/tailwind-stacked-cards/tailwind-scroll-activated-sticky-stacking-cards/
The How-It-Works sequence: each step pins, and the one behind it scales down and dims as the next slides over. An IntersectionObserver sets one data-state per card — ahead, live, behind — and every visual difference is a data-[state=…] variant, which is both cheaper than scroll maths and far easier to read.
## HTML
```html
<section class="stk-29 relative w-full min-h-svh bg-stk29-void font-stk29-sans text-white">
<div class="pointer-events-none fixed -inset-[30%] z-0 blur-[30px] animate-stk29-drift motion-reduce:animate-none [background-image:radial-gradient(40%_40%_at_25%_25%,var(--color-stk29-aur1),transparent_70%),radial-gradient(45%_45%_at_75%_30%,var(--color-stk29-aur2),transparent_70%),radial-gradient(50%_50%_at_50%_80%,var(--color-stk29-aur3),transparent_70%)]" aria-hidden="true"></div>
<div class="pointer-events-none fixed inset-0 z-0 mix-blend-overlay [background-image:repeating-linear-gradient(0deg,rgba(255,255,255,0.02)_0_1px,transparent_1px_3px)]" aria-hidden="true"></div>
<div class="fixed right-6 top-1/2 z-5 flex -translate-y-1/2 flex-col gap-3.5" id="stk-29-rail" aria-hidden="true">
<span class="block size-2 rounded-full bg-white/25 transition-all duration-400 data-[on=true]:h-6.5 data-[on=true]:rounded-md data-[on=true]:bg-white data-[on=true]:shadow-[0_0_14px_rgba(255,255,255,0.6)]" data-on="true"></span>
<span class="block size-2 rounded-full bg-white/25 transition-all duration-400 data-[on=true]:h-6.5 data-[on=true]:rounded-md data-[on=true]:bg-white data-[on=true]:shadow-[0_0_14px_rgba(255,255,255,0.6)]"></span>
<span class="block size-2 rounded-full bg-white/25 transition-all duration-400 data-[on=true]:h-6.5 data-[on=true]:rounded-md data-[on=true]:bg-white data-[on=true]:shadow-[0_0_14px_rgba(255,255,255,0.6)]"></span>
<span class="block size-2 rounded-full bg-white/25 transition-all duration-400 data-[on=true]:h-6.5 data-[on=true]:rounded-md data-[on=true]:bg-white data-[on=true]:shadow-[0_0_14px_rgba(255,255,255,0.6)]"></span>
</div>
<p class="sr-only" id="stk-29-live" aria-live="polite"></p>
<div class="relative z-1 mx-auto w-[min(460px,92vw)] pt-[55vh] pb-[75vh]" id="stk-29-steps">
<section class="sticky top-[20vh] mb-9 flex h-80 flex-col justify-between overflow-hidden rounded-[28px] p-9.5 origin-top backdrop-blur-[6px] shadow-[0_40px_80px_rgba(0,0,0,0.55),inset_0_0_0_1px_rgba(255,255,255,0.08)] transition-[transform,filter] duration-500 ease-stk29 data-[state=behind]:scale-[0.94] data-[state=behind]:brightness-[0.7] data-[state=behind]:-translate-y-2 motion-reduce:transition-none motion-reduce:scale-100! motion-reduce:brightness-100!" data-state="live" data-label="Discover">
<div class="absolute inset-0 -z-1 opacity-95 bg-[size:200%_200%] animate-stk29-flow motion-reduce:animate-none bg-linear-[135deg,var(--color-stk29-a1),var(--color-stk29-a2)_45%,var(--color-stk29-a3)]" aria-hidden="true"></div>
<div class="absolute -top-1/2 -left-[30%] h-[200%] w-1/2 animate-stk29-sweep motion-reduce:animate-none [background-image:linear-gradient(90deg,transparent,rgba(255,255,255,0.35),transparent)]" aria-hidden="true"></div>
<div class="flex items-center gap-3.5">
<span class="font-stk29-serif text-[4rem] italic leading-[0.8] opacity-90 [text-shadow:0_4px_20px_rgba(0,0,0,0.3)]">01</span>
<span class="ml-auto rounded-full border border-white/25 bg-white/18 px-3.5 py-1.75 text-[0.62rem] tracking-[0.22em] uppercase backdrop-blur-[6px]">Phase one</span>
</div>
<div>
<h2 class="m-0 font-stk29-serif text-[2.4rem] tracking-[-0.01em]">Discover</h2>
<p class="m-0 mt-1 max-w-[32ch] font-light leading-relaxed opacity-90">We map your goals and audit exactly where you stand today.</p>
<div class="mt-4 h-1 overflow-hidden rounded bg-white/25"><i class="block h-full w-1/4 rounded bg-white transition-[width] duration-500"></i></div>
</div>
</section>
<section class="sticky top-[20vh] mb-9 flex h-80 flex-col justify-between overflow-hidden rounded-[28px] p-9.5 origin-top backdrop-blur-[6px] shadow-[0_40px_80px_rgba(0,0,0,0.55),inset_0_0_0_1px_rgba(255,255,255,0.08)] transition-[transform,filter] duration-500 ease-stk29 data-[state=behind]:scale-[0.94] data-[state=behind]:brightness-[0.7] data-[state=behind]:-translate-y-2 motion-reduce:transition-none motion-reduce:scale-100! motion-reduce:brightness-100!" data-state="ahead" data-label="Design">
<div class="absolute inset-0 -z-1 opacity-95 bg-[size:200%_200%] animate-stk29-flow motion-reduce:animate-none bg-linear-[135deg,var(--color-stk29-b1),var(--color-stk29-b2)_45%,var(--color-stk29-b3)]" aria-hidden="true"></div>
<div class="absolute -top-1/2 -left-[30%] h-[200%] w-1/2 animate-stk29-sweep motion-reduce:animate-none [background-image:linear-gradient(90deg,transparent,rgba(255,255,255,0.35),transparent)]" aria-hidden="true"></div>
<div class="flex items-center gap-3.5">
<span class="font-stk29-serif text-[4rem] italic leading-[0.8] opacity-90 [text-shadow:0_4px_20px_rgba(0,0,0,0.3)]">02</span>
<span class="ml-auto rounded-full border border-white/25 bg-white/18 px-3.5 py-1.75 text-[0.62rem] tracking-[0.22em] uppercase backdrop-blur-[6px]">Phase two</span>
</div>
<div>
<h2 class="m-0 font-stk29-serif text-[2.4rem] tracking-[-0.01em]">Design</h2>
<p class="m-0 mt-1 max-w-[32ch] font-light leading-relaxed opacity-90">Concepts take shape through rapid, collaborative iteration.</p>
<div class="mt-4 h-1 overflow-hidden rounded bg-white/25"><i class="block h-full w-2/4 rounded bg-white transition-[width] duration-500"></i></div>
</div>
</section>
<section class="sticky top-[20vh] mb-9 flex h-80 flex-col justify-between overflow-hidden rounded-[28px] p-9.5 origin-top backdrop-blur-[6px] shadow-[0_40px_80px_rgba(0,0,0,0.55),inset_0_0_0_1px_rgba(255,255,255,0.08)] transition-[transform,filter] duration-500 ease-stk29 data-[state=behind]:scale-[0.94] data-[state=behind]:brightness-[0.7] data-[state=behind]:-translate-y-2 motion-reduce:transition-none motion-reduce:scale-100! motion-reduce:brightness-100!" data-state="ahead" data-label="Build">
<div class="absolute inset-0 -z-1 opacity-95 bg-[size:200%_200%] animate-stk29-flow motion-reduce:animate-none bg-linear-[135deg,var(--color-stk29-c1),var(--color-stk29-c2)_50%,var(--color-stk29-c3)]" aria-hidden="true"></div>
<div class="absolute -top-1/2 -left-[30%] h-[200%] w-1/2 animate-stk29-sweep motion-reduce:animate-none [background-image:linear-gradient(90deg,transparent,rgba(255,255,255,0.35),transparent)]" aria-hidden="true"></div>
<div class="flex items-center gap-3.5">
<span class="font-stk29-serif text-[4rem] italic leading-[0.8] opacity-90 [text-shadow:0_4px_20px_rgba(0,0,0,0.3)]">03</span>
<span class="ml-auto rounded-full border border-white/25 bg-white/18 px-3.5 py-1.75 text-[0.62rem] tracking-[0.22em] uppercase backdrop-blur-[6px]">Phase three</span>
</div>
<div>
<h2 class="m-0 font-stk29-serif text-[2.4rem] tracking-[-0.01em]">Build</h2>
<p class="m-0 mt-1 max-w-[32ch] font-light leading-relaxed opacity-90">Production-ready code, tested and shipped with real care.</p>
<div class="mt-4 h-1 overflow-hidden rounded bg-white/25"><i class="block h-full w-3/4 rounded bg-white transition-[width] duration-500"></i></div>
</div>
</section>
<section class="sticky top-[20vh] mb-9 flex h-80 flex-col justify-between overflow-hidden rounded-[28px] p-9.5 origin-top backdrop-blur-[6px] shadow-[0_40px_80px_rgba(0,0,0,0.55),inset_0_0_0_1px_rgba(255,255,255,0.08)] transition-[transform,filter] duration-500 ease-stk29 data-[state=behind]:scale-[0.94] data-[state=behind]:brightness-[0.7] data-[state=behind]:-translate-y-2 motion-reduce:transition-none motion-reduce:scale-100! motion-reduce:brightness-100!" data-state="ahead" data-label="Launch">
<div class="absolute inset-0 -z-1 opacity-95 bg-[size:200%_200%] animate-stk29-flow motion-reduce:animate-none bg-linear-[135deg,var(--color-stk29-d1),var(--color-stk29-d2)_45%,var(--color-stk29-d3)]" aria-hidden="true"></div>
<div class="absolute -top-1/2 -left-[30%] h-[200%] w-1/2 animate-stk29-sweep motion-reduce:animate-none [background-image:linear-gradient(90deg,transparent,rgba(255,255,255,0.35),transparent)]" aria-hidden="true"></div>
<div class="flex items-center gap-3.5">
<span class="font-stk29-serif text-[4rem] italic leading-[0.8] opacity-90 [text-shadow:0_4px_20px_rgba(0,0,0,0.3)]">04</span>
<span class="ml-auto rounded-full border border-white/25 bg-white/18 px-3.5 py-1.75 text-[0.62rem] tracking-[0.22em] uppercase backdrop-blur-[6px]">Phase four</span>
</div>
<div>
<h2 class="m-0 font-stk29-serif text-[2.4rem] tracking-[-0.01em]">Launch</h2>
<p class="m-0 mt-1 max-w-[32ch] font-light leading-relaxed opacity-90">We go live, measure everything, then refine what works.</p>
<div class="mt-4 h-1 overflow-hidden rounded bg-white/25"><i class="block h-full w-full rounded bg-white transition-[width] duration-500"></i></div>
</div>
</section>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-stk29-void: #05060a;
--color-stk29-void: oklch(0.08 0.012 275);
--color-stk29-aur1: rgba(99, 102, 241, 0.30);
--color-stk29-aur2: rgba(236, 72, 153, 0.26);
--color-stk29-aur3: rgba(45, 212, 191, 0.24);
--color-stk29-a1: #1e1b4b;
--color-stk29-a2: #4338ca;
--color-stk29-a3: #6d28d9;
--color-stk29-b1: #831843;
--color-stk29-b2: #be185d;
--color-stk29-b3: #f43f5e;
--color-stk29-c1: #064e3b;
--color-stk29-c2: #0d9488;
--color-stk29-c3: #2dd4bf;
--color-stk29-d1: #7c2d12;
--color-stk29-d2: #ea580c;
--color-stk29-d3: #f59e0b;
--font-stk29-sans: 'Geist', system-ui, sans-serif;
--font-stk29-serif: 'Instrument Serif', Georgia, serif;
--ease-stk29: cubic-bezier(0.3, 0.9, 0.3, 1);
--animate-stk29-flow: stk-29-flow 9s ease infinite;
--animate-stk29-drift: stk-29-drift 20s ease-in-out infinite alternate;
--animate-stk29-sweep: stk-29-sweep 6s ease-in-out infinite;
}
@keyframes stk-29-flow {
0%,
100% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
}
@keyframes stk-29-drift {
from {
transform: translate(-3%, -2%) scale(1);
}
to {
transform: translate(4%, 3%) scale(1.1);
}
}
@keyframes stk-29-sweep {
0%,
100% {
transform: rotate(18deg) translateX(-300px);
}
50% {
transform: rotate(18deg) translateX(700px);
}
}
```
## JavaScript
```js
(() => {
const root = document.querySelector('.stk-29');
if (!root) return;
const steps = [...root.querySelectorAll('#stk-29-steps > section')];
const pips = [...root.querySelectorAll('#stk-29-rail > span')];
const live = root.querySelector('#stk-29-live');
let current = -1;
function setCurrent(i) {
if (i === current) return;
current = i;
steps.forEach((s, n) => { s.dataset.state = n < i ? 'behind' : n === i ? 'live' : 'ahead'; });
pips.forEach((p, n) => { if (n === i) p.dataset.on = 'true'; else delete p.dataset.on; });
/* A pin is a visual event and nothing more — say which step it is. */
live.textContent = 'Step ' + (i + 1) + ' of ' + steps.length + ': ' + steps[i].dataset.label;
}
/* An observer, not a scroll listener: nothing here runs per frame and no
layout is read during scrolling. */
const io = new IntersectionObserver((entries) => {
entries.forEach((e) => {
if (e.isIntersecting) setCurrent(steps.indexOf(e.target));
});
}, { rootMargin: '-20% 0px -60% 0px', threshold: 0 });
steps.forEach((s) => io.observe(s));
setCurrent(0);
})();
```Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind Scroll Activated Sticky Stacking Cards
Source: https://codefronts.com/components/tailwind-stacked-cards/tailwind-scroll-activated-sticky-stacking-cards/
The How-It-Works sequence: each step pins, and the one behind it scales down and dims as the next slides over. An IntersectionObserver sets one data-state per card — ahead, live, behind — and every visual difference is a data-[state=…] variant, which is both cheaper than scroll maths and far easier to read.
## HTML
```html
<section class="stk-29 relative w-full min-h-svh bg-stk29-void font-stk29-sans text-white">
<div class="pointer-events-none fixed -inset-[30%] z-0 blur-[30px] animate-stk29-drift motion-reduce:animate-none [background-image:radial-gradient(40%_40%_at_25%_25%,var(--color-stk29-aur1),transparent_70%),radial-gradient(45%_45%_at_75%_30%,var(--color-stk29-aur2),transparent_70%),radial-gradient(50%_50%_at_50%_80%,var(--color-stk29-aur3),transparent_70%)]" aria-hidden="true"></div>
<div class="pointer-events-none fixed inset-0 z-0 mix-blend-overlay [background-image:repeating-linear-gradient(0deg,rgba(255,255,255,0.02)_0_1px,transparent_1px_3px)]" aria-hidden="true"></div>
<div class="fixed right-6 top-1/2 z-5 flex -translate-y-1/2 flex-col gap-3.5" id="stk-29-rail" aria-hidden="true">
<span class="block size-2 rounded-full bg-white/25 transition-all duration-400 data-[on=true]:h-6.5 data-[on=true]:rounded-md data-[on=true]:bg-white data-[on=true]:shadow-[0_0_14px_rgba(255,255,255,0.6)]" data-on="true"></span>
<span class="block size-2 rounded-full bg-white/25 transition-all duration-400 data-[on=true]:h-6.5 data-[on=true]:rounded-md data-[on=true]:bg-white data-[on=true]:shadow-[0_0_14px_rgba(255,255,255,0.6)]"></span>
<span class="block size-2 rounded-full bg-white/25 transition-all duration-400 data-[on=true]:h-6.5 data-[on=true]:rounded-md data-[on=true]:bg-white data-[on=true]:shadow-[0_0_14px_rgba(255,255,255,0.6)]"></span>
<span class="block size-2 rounded-full bg-white/25 transition-all duration-400 data-[on=true]:h-6.5 data-[on=true]:rounded-md data-[on=true]:bg-white data-[on=true]:shadow-[0_0_14px_rgba(255,255,255,0.6)]"></span>
</div>
<p class="sr-only" id="stk-29-live" aria-live="polite"></p>
<div class="relative z-1 mx-auto w-[min(460px,92vw)] pt-[55vh] pb-[75vh]" id="stk-29-steps">
<section class="sticky top-[20vh] mb-9 flex h-80 flex-col justify-between overflow-hidden rounded-[28px] p-9.5 origin-top backdrop-blur-[6px] shadow-[0_40px_80px_rgba(0,0,0,0.55),inset_0_0_0_1px_rgba(255,255,255,0.08)] transition-[transform,filter] duration-500 ease-stk29 data-[state=behind]:scale-[0.94] data-[state=behind]:brightness-[0.7] data-[state=behind]:-translate-y-2 motion-reduce:transition-none motion-reduce:scale-100! motion-reduce:brightness-100!" data-state="live" data-label="Discover">
<div class="absolute inset-0 -z-1 opacity-95 bg-[size:200%_200%] animate-stk29-flow motion-reduce:animate-none bg-linear-[135deg,var(--color-stk29-a1),var(--color-stk29-a2)_45%,var(--color-stk29-a3)]" aria-hidden="true"></div>
<div class="absolute -top-1/2 -left-[30%] h-[200%] w-1/2 animate-stk29-sweep motion-reduce:animate-none [background-image:linear-gradient(90deg,transparent,rgba(255,255,255,0.35),transparent)]" aria-hidden="true"></div>
<div class="flex items-center gap-3.5">
<span class="font-stk29-serif text-[4rem] italic leading-[0.8] opacity-90 [text-shadow:0_4px_20px_rgba(0,0,0,0.3)]">01</span>
<span class="ml-auto rounded-full border border-white/25 bg-white/18 px-3.5 py-1.75 text-[0.62rem] tracking-[0.22em] uppercase backdrop-blur-[6px]">Phase one</span>
</div>
<div>
<h2 class="m-0 font-stk29-serif text-[2.4rem] tracking-[-0.01em]">Discover</h2>
<p class="m-0 mt-1 max-w-[32ch] font-light leading-relaxed opacity-90">We map your goals and audit exactly where you stand today.</p>
<div class="mt-4 h-1 overflow-hidden rounded bg-white/25"><i class="block h-full w-1/4 rounded bg-white transition-[width] duration-500"></i></div>
</div>
</section>
<section class="sticky top-[20vh] mb-9 flex h-80 flex-col justify-between overflow-hidden rounded-[28px] p-9.5 origin-top backdrop-blur-[6px] shadow-[0_40px_80px_rgba(0,0,0,0.55),inset_0_0_0_1px_rgba(255,255,255,0.08)] transition-[transform,filter] duration-500 ease-stk29 data-[state=behind]:scale-[0.94] data-[state=behind]:brightness-[0.7] data-[state=behind]:-translate-y-2 motion-reduce:transition-none motion-reduce:scale-100! motion-reduce:brightness-100!" data-state="ahead" data-label="Design">
<div class="absolute inset-0 -z-1 opacity-95 bg-[size:200%_200%] animate-stk29-flow motion-reduce:animate-none bg-linear-[135deg,var(--color-stk29-b1),var(--color-stk29-b2)_45%,var(--color-stk29-b3)]" aria-hidden="true"></div>
<div class="absolute -top-1/2 -left-[30%] h-[200%] w-1/2 animate-stk29-sweep motion-reduce:animate-none [background-image:linear-gradient(90deg,transparent,rgba(255,255,255,0.35),transparent)]" aria-hidden="true"></div>
<div class="flex items-center gap-3.5">
<span class="font-stk29-serif text-[4rem] italic leading-[0.8] opacity-90 [text-shadow:0_4px_20px_rgba(0,0,0,0.3)]">02</span>
<span class="ml-auto rounded-full border border-white/25 bg-white/18 px-3.5 py-1.75 text-[0.62rem] tracking-[0.22em] uppercase backdrop-blur-[6px]">Phase two</span>
</div>
<div>
<h2 class="m-0 font-stk29-serif text-[2.4rem] tracking-[-0.01em]">Design</h2>
<p class="m-0 mt-1 max-w-[32ch] font-light leading-relaxed opacity-90">Concepts take shape through rapid, collaborative iteration.</p>
<div class="mt-4 h-1 overflow-hidden rounded bg-white/25"><i class="block h-full w-2/4 rounded bg-white transition-[width] duration-500"></i></div>
</div>
</section>
<section class="sticky top-[20vh] mb-9 flex h-80 flex-col justify-between overflow-hidden rounded-[28px] p-9.5 origin-top backdrop-blur-[6px] shadow-[0_40px_80px_rgba(0,0,0,0.55),inset_0_0_0_1px_rgba(255,255,255,0.08)] transition-[transform,filter] duration-500 ease-stk29 data-[state=behind]:scale-[0.94] data-[state=behind]:brightness-[0.7] data-[state=behind]:-translate-y-2 motion-reduce:transition-none motion-reduce:scale-100! motion-reduce:brightness-100!" data-state="ahead" data-label="Build">
<div class="absolute inset-0 -z-1 opacity-95 bg-[size:200%_200%] animate-stk29-flow motion-reduce:animate-none bg-linear-[135deg,var(--color-stk29-c1),var(--color-stk29-c2)_50%,var(--color-stk29-c3)]" aria-hidden="true"></div>
<div class="absolute -top-1/2 -left-[30%] h-[200%] w-1/2 animate-stk29-sweep motion-reduce:animate-none [background-image:linear-gradient(90deg,transparent,rgba(255,255,255,0.35),transparent)]" aria-hidden="true"></div>
<div class="flex items-center gap-3.5">
<span class="font-stk29-serif text-[4rem] italic leading-[0.8] opacity-90 [text-shadow:0_4px_20px_rgba(0,0,0,0.3)]">03</span>
<span class="ml-auto rounded-full border border-white/25 bg-white/18 px-3.5 py-1.75 text-[0.62rem] tracking-[0.22em] uppercase backdrop-blur-[6px]">Phase three</span>
</div>
<div>
<h2 class="m-0 font-stk29-serif text-[2.4rem] tracking-[-0.01em]">Build</h2>
<p class="m-0 mt-1 max-w-[32ch] font-light leading-relaxed opacity-90">Production-ready code, tested and shipped with real care.</p>
<div class="mt-4 h-1 overflow-hidden rounded bg-white/25"><i class="block h-full w-3/4 rounded bg-white transition-[width] duration-500"></i></div>
</div>
</section>
<section class="sticky top-[20vh] mb-9 flex h-80 flex-col justify-between overflow-hidden rounded-[28px] p-9.5 origin-top backdrop-blur-[6px] shadow-[0_40px_80px_rgba(0,0,0,0.55),inset_0_0_0_1px_rgba(255,255,255,0.08)] transition-[transform,filter] duration-500 ease-stk29 data-[state=behind]:scale-[0.94] data-[state=behind]:brightness-[0.7] data-[state=behind]:-translate-y-2 motion-reduce:transition-none motion-reduce:scale-100! motion-reduce:brightness-100!" data-state="ahead" data-label="Launch">
<div class="absolute inset-0 -z-1 opacity-95 bg-[size:200%_200%] animate-stk29-flow motion-reduce:animate-none bg-linear-[135deg,var(--color-stk29-d1),var(--color-stk29-d2)_45%,var(--color-stk29-d3)]" aria-hidden="true"></div>
<div class="absolute -top-1/2 -left-[30%] h-[200%] w-1/2 animate-stk29-sweep motion-reduce:animate-none [background-image:linear-gradient(90deg,transparent,rgba(255,255,255,0.35),transparent)]" aria-hidden="true"></div>
<div class="flex items-center gap-3.5">
<span class="font-stk29-serif text-[4rem] italic leading-[0.8] opacity-90 [text-shadow:0_4px_20px_rgba(0,0,0,0.3)]">04</span>
<span class="ml-auto rounded-full border border-white/25 bg-white/18 px-3.5 py-1.75 text-[0.62rem] tracking-[0.22em] uppercase backdrop-blur-[6px]">Phase four</span>
</div>
<div>
<h2 class="m-0 font-stk29-serif text-[2.4rem] tracking-[-0.01em]">Launch</h2>
<p class="m-0 mt-1 max-w-[32ch] font-light leading-relaxed opacity-90">We go live, measure everything, then refine what works.</p>
<div class="mt-4 h-1 overflow-hidden rounded bg-white/25"><i class="block h-full w-full rounded bg-white transition-[width] duration-500"></i></div>
</div>
</section>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-stk29-void: #05060a;
--color-stk29-void: oklch(0.08 0.012 275);
--color-stk29-aur1: rgba(99, 102, 241, 0.30);
--color-stk29-aur2: rgba(236, 72, 153, 0.26);
--color-stk29-aur3: rgba(45, 212, 191, 0.24);
--color-stk29-a1: #1e1b4b;
--color-stk29-a2: #4338ca;
--color-stk29-a3: #6d28d9;
--color-stk29-b1: #831843;
--color-stk29-b2: #be185d;
--color-stk29-b3: #f43f5e;
--color-stk29-c1: #064e3b;
--color-stk29-c2: #0d9488;
--color-stk29-c3: #2dd4bf;
--color-stk29-d1: #7c2d12;
--color-stk29-d2: #ea580c;
--color-stk29-d3: #f59e0b;
--font-stk29-sans: 'Geist', system-ui, sans-serif;
--font-stk29-serif: 'Instrument Serif', Georgia, serif;
--ease-stk29: cubic-bezier(0.3, 0.9, 0.3, 1);
--animate-stk29-flow: stk-29-flow 9s ease infinite;
--animate-stk29-drift: stk-29-drift 20s ease-in-out infinite alternate;
--animate-stk29-sweep: stk-29-sweep 6s ease-in-out infinite;
}
@keyframes stk-29-flow {
0%,
100% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
}
@keyframes stk-29-drift {
from {
transform: translate(-3%, -2%) scale(1);
}
to {
transform: translate(4%, 3%) scale(1.1);
}
}
@keyframes stk-29-sweep {
0%,
100% {
transform: rotate(18deg) translateX(-300px);
}
50% {
transform: rotate(18deg) translateX(700px);
}
}
```
## JavaScript
```js
(() => {
const root = document.querySelector('.stk-29');
if (!root) return;
const steps = [...root.querySelectorAll('#stk-29-steps > section')];
const pips = [...root.querySelectorAll('#stk-29-rail > span')];
const live = root.querySelector('#stk-29-live');
let current = -1;
function setCurrent(i) {
if (i === current) return;
current = i;
steps.forEach((s, n) => { s.dataset.state = n < i ? 'behind' : n === i ? 'live' : 'ahead'; });
pips.forEach((p, n) => { if (n === i) p.dataset.on = 'true'; else delete p.dataset.on; });
/* A pin is a visual event and nothing more — say which step it is. */
live.textContent = 'Step ' + (i + 1) + ' of ' + steps.length + ': ' + steps[i].dataset.label;
}
/* An observer, not a scroll listener: nothing here runs per frame and no
layout is read during scrolling. */
const io = new IntersectionObserver((entries) => {
entries.forEach((e) => {
if (e.isIntersecting) setCurrent(steps.indexOf(e.target));
});
}, { rootMargin: '-20% 0px -60% 0px', threshold: 0 });
steps.forEach((s) => io.observe(s));
setCurrent(0);
})();
```How this works
Observe, do not listen. A scroll handler that measures each card runs dozens of times a second and reads layout every time. An IntersectionObserver with a threshold does the same job off the main thread and hands you a boolean. The controller's entire styling job is one attribute:
card.dataset.state = 'live' | 'behind' | 'ahead';The recession is three variants. A pinned card that has been passed gets scale-[0.94], brightness-[0.7] and a small -translate-y, all under data-[state=behind]:. origin-top is the detail that sells it: scaling from the centre makes the card appear to shrink, scaling from the top makes it appear to sink backwards under the next one.
Sticky needs an un-clipped ancestor. position:sticky fails silently if any ancestor has overflow:hidden — the single most common reason a sticky stack ‘does not work’. This demo keeps the scroll on the page and puts no overflow utility anywhere in the chain.
Pinning is not progress. A screen-reader user gets nothing from a card sticking to the top of the viewport, so the live region announces each step by name as it becomes current, and the pip rail is aria-hidden decoration on top of that.
Make it yours
- Change the pin offset via
sticky top-[20vh]— tighter for a fixed nav, looser for breathing room. - Tune the recession with the
0.94scale and0.7brightness in thedata-[state=behind]variants. - Add a step by copying a
<section>and a pip; the observer reads both from the DOM. - Rebrand each step through its own
bg-linear-[...]; the animated flow is a shared token. - Drop the sheen entirely — it is one element per card and purely decorative.
Gotchas — read before shipping
position:stickyfails silently under any ancestor withoverflow:hidden. If a sticky stack does nothing, check the ancestors before anything else.- Use
origin-topon the recede, or the passed card shrinks toward its own centre and the stack looks like it is deflating. - Prefer
IntersectionObserverto a scroll listener; per-framegetBoundingClientRect()during scroll is a direct INP cost. - Every step's content must be complete at first paint — a lazy image inside a pinned card shifts layout while it is pinned.
- Announce step changes with
aria-live. The pin itself conveys nothing to assistive technology. - Under reduced motion, stop the recede as well as the decorative loops; the cards then simply scroll past one another.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
position:sticky and IntersectionObserver are universal (Chrome 56+, Safari 12.1+, Firefox 55+). The floor is Tailwind v4's own generated sheet.