36 Tailwind Stacked Cards
29 / 36

All 36 in 36 Tailwind Stacked Cards

  1. 01Tailwind Scroll-Driven Scaling Card Stack
  2. 02Tailwind Swipe Card Deck Drag to Dismiss
  3. 03Tailwind Glassmorphism Testimonial Card Stack
  4. 04Tailwind Stacked Cards Expand Accordion on Click
  5. 05Tailwind Fan Out Photo Stack on Hover
  6. 06Tailwind 3D Perspective Tilt Card Stack
  7. 07Tailwind Multi Step Form Card Stack
  8. 08Tailwind Tabbed Card Stack Bring to Front
  9. 09Tailwind Click to Front Stacked Cards Cycle
  10. 10Tailwind Classic Card Deck Stack Shadow
  11. 11Tailwind Fan Spread Hand of Cards
  12. 12Tailwind Polaroid Photo Stack Scattered Rotate
  13. 13Tailwind Staircase Offset Stacked Cards Cascade
  14. 14Tailwind Cards Spread Out Horizontally on Hover
  15. 15Tailwind Stacked Cards Lift on Hover Sibling Push
  16. 16Tailwind Accordion Stacked Cards Slide Out Menu
  17. 17Tailwind Shuffle Cards Animation Loop Deck
  18. 18Tailwind File Folder Tab Stacked Cards
  19. 19Tailwind Scattered Photos Pinch to Centre on Hover
  20. 20Tailwind Mobile Swipe Card Stack Translate3d
  21. 21Tailwind Single Accordion Card Nested Layers
  22. 22Tailwind Isometric Tower Stacked Cards 3D Column
  23. 23Tailwind 3D Flip Card Stack Reveal Back
  24. 24Tailwind Card Stack Tilt on Hover Quadrant
  25. 25Tailwind Notification Stack Pile Dismiss
  26. 26Tailwind Receipt Roll Unroll Stacked Items
  27. 27Tailwind Scratch Card Reveal Canvas Mask
  28. 28Tailwind Stacked Cards Hover Reveal Spread
  29. 29Tailwind Scroll Activated Sticky Stacking Cards
  30. 30Tailwind Stacked Testimonial Slider Radio
  31. 31Tailwind Isometric Overlapping Stacked Cards
  32. 32Tailwind Bento Grid Stacked Cards Hover Fan
  33. 33Tailwind Perspective Card Deck Depth Tunnel
  34. 34Tailwind Peel Back Corner Card Reveal
  35. 35Tailwind Timeline Stacked Milestone Cards
  36. 36Tailwind Spiral Tower Rotating Stacked Cards
Tailwind v4MIT licensed

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.

Live Demo
Plain CSSTailwind v4
Try it

Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time

Untitled UI React · Affiliate link

Get Untitled UI Pro

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>
@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);
})();

Setup — do this once per project

Skip this if your project already runs Tailwind v4.

1Load Tailwind
<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.

2Add the stylesheetRequiredthe @theme block is what makes the custom utilities exist
@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);
  }
}
3Fonts used by this demooptional — skip if you have your own type stack
<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 in setup, not as an @import inside the stylesheet. Tailwind emits user @import rules after its own @layer blocks, and a late @import is 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-state per card and the recession lives in data-[state=behind]: variants; the script contains no styles at all.
  • origin-top is 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 marked aria-hidden, with the real progress carried by the aria-live region beside it.
  • No overflow utility appears anywhere in the ancestor chain, because position:sticky fails silently under a clipped ancestor — the commonest reason a sticky stack &lsquo;does not work&rsquo;.
  • The reduced-motion path uses motion-reduce:scale-100! and motion-reduce:brightness-100! to beat the data-[state=behind] variants, so the cards just scroll past one another.
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
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 &mdash; ahead, live, behind &mdash; 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.94 scale and 0.7 brightness in the data-[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:sticky fails silently under any ancestor with overflow:hidden. If a sticky stack does nothing, check the ancestors before anything else.
  • Use origin-top on the recede, or the passed card shrinks toward its own centre and the stack looks like it is deflating.
  • Prefer IntersectionObserver to a scroll listener; per-frame getBoundingClientRect() 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

ChromeSafariFirefoxEdge
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.

Techniques used in this demo

Theme tokensset in @theme, used by a utility

Arbitraryneeds bracket syntax

Plain CSSno Tailwind surface

Search CodeFronts

Loading…