01 / 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-Driven Scaling Card Stack
The storytelling hero, written in the markup. Each card is sticky with a stepped offset computed from its own --i, so top-[calc(90px+var(--i)*16px)] replaces four :nth-child rules. The scale-and-darken is an @theme animation — animate-stk01-shrink — driven by the two arbitrary properties Tailwind has no utility for, [animation-timeline:view()] and [animation-range:exit]. The only rules left in a stylesheet are the keyframes themselves.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="stk-01 w-full min-h-svh h-svh overflow-y-auto bg-stk01-paper font-stk01-sans px-5 [scroll-behavior:smooth] motion-reduce:[scroll-behavior:auto]" aria-label="Scroll-driven scaling card stack">
<div class="w-[min(520px,100%)] mx-auto pt-[14vh] pb-[40vh]">
<p class="sticky top-4 z-10 m-0 text-center font-stk01-mono text-[11px] tracking-[0.16em] uppercase text-stk01-muted mb-[6vh]">Scroll ↓ inside this panel</p>
<article class="sticky top-[calc(90px+var(--i)*16px)] h-65 mb-15 rounded-[22px] p-[34px] text-white bg-linear-[135deg,var(--color-stk01-a1),var(--color-stk01-a2)] shadow-[0_30px_60px_-34px_rgba(20,24,50,0.7)] animate-stk01-shrink [animation-timeline:view()] [animation-range:exit] motion-reduce:animate-none" style="--i:0">
<span class="font-stk01-mono text-[13px] font-medium tracking-[0.2em] opacity-70">01</span>
<h3 class="mt-3.5 mb-2.5 text-3xl font-extrabold tracking-[-0.02em]">Capture</h3>
<p class="max-w-[34ch] text-base leading-relaxed opacity-90">Every idea starts as a rough note. We pin it before it escapes.</p>
</article>
<article class="sticky top-[calc(90px+var(--i)*16px)] h-65 mb-15 rounded-[22px] p-[34px] text-white bg-linear-[135deg,var(--color-stk01-b1),var(--color-stk01-b2)] shadow-[0_30px_60px_-34px_rgba(20,24,50,0.7)] animate-stk01-shrink [animation-timeline:view()] [animation-range:exit] motion-reduce:animate-none" style="--i:1">
<span class="font-stk01-mono text-[13px] font-medium tracking-[0.2em] opacity-70">02</span>
<h3 class="mt-3.5 mb-2.5 text-3xl font-extrabold tracking-[-0.02em]">Shape</h3>
<p class="max-w-[34ch] text-base leading-relaxed opacity-90">Notes compress into a plan — the earlier one settles quietly behind.</p>
</article>
<article class="sticky top-[calc(90px+var(--i)*16px)] h-65 mb-15 rounded-[22px] p-[34px] text-white bg-linear-[135deg,var(--color-stk01-c1),var(--color-stk01-c2)] shadow-[0_30px_60px_-34px_rgba(20,24,50,0.7)] animate-stk01-shrink [animation-timeline:view()] [animation-range:exit] motion-reduce:animate-none" style="--i:2">
<span class="font-stk01-mono text-[13px] font-medium tracking-[0.2em] opacity-70">03</span>
<h3 class="mt-3.5 mb-2.5 text-3xl font-extrabold tracking-[-0.02em]">Ship</h3>
<p class="max-w-[34ch] text-base leading-relaxed opacity-90">The plan surfaces on top, in focus, ready to hand off.</p>
</article>
<article class="sticky top-[calc(90px+var(--i)*16px)] h-65 mb-15 rounded-[22px] p-[34px] text-white bg-linear-[135deg,var(--color-stk01-d1),var(--color-stk01-d2)] shadow-[0_30px_60px_-34px_rgba(20,24,50,0.7)] animate-stk01-shrink [animation-timeline:view()] [animation-range:exit] motion-reduce:animate-none" style="--i:3">
<span class="font-stk01-mono text-[13px] font-medium tracking-[0.2em] opacity-70">04</span>
<h3 class="mt-3.5 mb-2.5 text-3xl font-extrabold tracking-[-0.02em]">Reflect</h3>
<p class="max-w-[34ch] text-base leading-relaxed opacity-90">What worked stacks up as depth you can scroll back through.</p>
</article>
<div class="pt-[12vh] text-center font-stk01-mono text-sm tracking-[0.14em] uppercase text-stk01-muted">Fin.</div>
</div>
</section><section class="stk-01 w-full min-h-svh h-svh overflow-y-auto bg-stk01-paper font-stk01-sans px-5 [scroll-behavior:smooth] motion-reduce:[scroll-behavior:auto]" aria-label="Scroll-driven scaling card stack">
<div class="w-[min(520px,100%)] mx-auto pt-[14vh] pb-[40vh]">
<p class="sticky top-4 z-10 m-0 text-center font-stk01-mono text-[11px] tracking-[0.16em] uppercase text-stk01-muted mb-[6vh]">Scroll ↓ inside this panel</p>
<article class="sticky top-[calc(90px+var(--i)*16px)] h-65 mb-15 rounded-[22px] p-[34px] text-white bg-linear-[135deg,var(--color-stk01-a1),var(--color-stk01-a2)] shadow-[0_30px_60px_-34px_rgba(20,24,50,0.7)] animate-stk01-shrink [animation-timeline:view()] [animation-range:exit] motion-reduce:animate-none" style="--i:0">
<span class="font-stk01-mono text-[13px] font-medium tracking-[0.2em] opacity-70">01</span>
<h3 class="mt-3.5 mb-2.5 text-3xl font-extrabold tracking-[-0.02em]">Capture</h3>
<p class="max-w-[34ch] text-base leading-relaxed opacity-90">Every idea starts as a rough note. We pin it before it escapes.</p>
</article>
<article class="sticky top-[calc(90px+var(--i)*16px)] h-65 mb-15 rounded-[22px] p-[34px] text-white bg-linear-[135deg,var(--color-stk01-b1),var(--color-stk01-b2)] shadow-[0_30px_60px_-34px_rgba(20,24,50,0.7)] animate-stk01-shrink [animation-timeline:view()] [animation-range:exit] motion-reduce:animate-none" style="--i:1">
<span class="font-stk01-mono text-[13px] font-medium tracking-[0.2em] opacity-70">02</span>
<h3 class="mt-3.5 mb-2.5 text-3xl font-extrabold tracking-[-0.02em]">Shape</h3>
<p class="max-w-[34ch] text-base leading-relaxed opacity-90">Notes compress into a plan — the earlier one settles quietly behind.</p>
</article>
<article class="sticky top-[calc(90px+var(--i)*16px)] h-65 mb-15 rounded-[22px] p-[34px] text-white bg-linear-[135deg,var(--color-stk01-c1),var(--color-stk01-c2)] shadow-[0_30px_60px_-34px_rgba(20,24,50,0.7)] animate-stk01-shrink [animation-timeline:view()] [animation-range:exit] motion-reduce:animate-none" style="--i:2">
<span class="font-stk01-mono text-[13px] font-medium tracking-[0.2em] opacity-70">03</span>
<h3 class="mt-3.5 mb-2.5 text-3xl font-extrabold tracking-[-0.02em]">Ship</h3>
<p class="max-w-[34ch] text-base leading-relaxed opacity-90">The plan surfaces on top, in focus, ready to hand off.</p>
</article>
<article class="sticky top-[calc(90px+var(--i)*16px)] h-65 mb-15 rounded-[22px] p-[34px] text-white bg-linear-[135deg,var(--color-stk01-d1),var(--color-stk01-d2)] shadow-[0_30px_60px_-34px_rgba(20,24,50,0.7)] animate-stk01-shrink [animation-timeline:view()] [animation-range:exit] motion-reduce:animate-none" style="--i:3">
<span class="font-stk01-mono text-[13px] font-medium tracking-[0.2em] opacity-70">04</span>
<h3 class="mt-3.5 mb-2.5 text-3xl font-extrabold tracking-[-0.02em]">Reflect</h3>
<p class="max-w-[34ch] text-base leading-relaxed opacity-90">What worked stacks up as depth you can scroll back through.</p>
</article>
<div class="pt-[12vh] text-center font-stk01-mono text-sm tracking-[0.14em] uppercase text-stk01-muted">Fin.</div>
</div>
</section>@import "tailwindcss";
@theme {
--color-stk01-paper: #eef0f4;
--color-stk01-paper: oklch(0.945 0.006 265);
--color-stk01-muted: #8a90a0;
--color-stk01-muted: oklch(0.64 0.018 265);
--color-stk01-a1: #6f6bf0;
--color-stk01-a1: oklch(0.62 0.17 274);
--color-stk01-a2: #8f5ce8;
--color-stk01-a2: oklch(0.55 0.16 292);
--color-stk01-b1: #3ec1d3;
--color-stk01-b1: oklch(0.66 0.15 200);
--color-stk01-b2: #4f8ee0;
--color-stk01-b2: oklch(0.60 0.16 250);
--color-stk01-c1: #4fd39a;
--color-stk01-c1: oklch(0.70 0.15 150);
--color-stk01-c2: #3bb7cf;
--color-stk01-c2: oklch(0.62 0.15 200);
--color-stk01-d1: #e9a53f;
--color-stk01-d1: oklch(0.72 0.16 60);
--color-stk01-d2: #e0713f;
--color-stk01-d2: oklch(0.64 0.17 30);
--font-stk01-sans: 'Archivo', system-ui, sans-serif;
--font-stk01-mono: 'JetBrains Mono', ui-monospace, monospace;
--animate-stk01-shrink: stk-01-shrink linear both;
}
/* Keyframes are a rule, not a value: there is no utility form for them, and the
@theme --animate-* entry above only names this block. */
@keyframes stk-01-shrink {
to {
transform: scale(0.9);
filter: brightness(0.72);
}
}@import "tailwindcss";
@theme {
--color-stk01-paper: #eef0f4;
--color-stk01-paper: oklch(0.945 0.006 265);
--color-stk01-muted: #8a90a0;
--color-stk01-muted: oklch(0.64 0.018 265);
--color-stk01-a1: #6f6bf0;
--color-stk01-a1: oklch(0.62 0.17 274);
--color-stk01-a2: #8f5ce8;
--color-stk01-a2: oklch(0.55 0.16 292);
--color-stk01-b1: #3ec1d3;
--color-stk01-b1: oklch(0.66 0.15 200);
--color-stk01-b2: #4f8ee0;
--color-stk01-b2: oklch(0.60 0.16 250);
--color-stk01-c1: #4fd39a;
--color-stk01-c1: oklch(0.70 0.15 150);
--color-stk01-c2: #3bb7cf;
--color-stk01-c2: oklch(0.62 0.15 200);
--color-stk01-d1: #e9a53f;
--color-stk01-d1: oklch(0.72 0.16 60);
--color-stk01-d2: #e0713f;
--color-stk01-d2: oklch(0.64 0.17 30);
--font-stk01-sans: 'Archivo', system-ui, sans-serif;
--font-stk01-mono: 'JetBrains Mono', ui-monospace, monospace;
--animate-stk01-shrink: stk-01-shrink linear both;
}
/* Keyframes are a rule, not a value: there is no utility form for them, and the
@theme --animate-* entry above only names this block. */
@keyframes stk-01-shrink {
to {
transform: scale(0.9);
filter: brightness(0.72);
}
}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-stk01-paper: #eef0f4;
--color-stk01-paper: oklch(0.945 0.006 265);
--color-stk01-muted: #8a90a0;
--color-stk01-muted: oklch(0.64 0.018 265);
--color-stk01-a1: #6f6bf0;
--color-stk01-a1: oklch(0.62 0.17 274);
--color-stk01-a2: #8f5ce8;
--color-stk01-a2: oklch(0.55 0.16 292);
--color-stk01-b1: #3ec1d3;
--color-stk01-b1: oklch(0.66 0.15 200);
--color-stk01-b2: #4f8ee0;
--color-stk01-b2: oklch(0.60 0.16 250);
--color-stk01-c1: #4fd39a;
--color-stk01-c1: oklch(0.70 0.15 150);
--color-stk01-c2: #3bb7cf;
--color-stk01-c2: oklch(0.62 0.15 200);
--color-stk01-d1: #e9a53f;
--color-stk01-d1: oklch(0.72 0.16 60);
--color-stk01-d2: #e0713f;
--color-stk01-d2: oklch(0.64 0.17 30);
--font-stk01-sans: 'Archivo', system-ui, sans-serif;
--font-stk01-mono: 'JetBrains Mono', ui-monospace, monospace;
--animate-stk01-shrink: stk-01-shrink linear both;
}
/* Keyframes are a rule, not a value: there is no utility form for them, and the
@theme --animate-* entry above only names this block. */
@keyframes stk-01-shrink {
to {
transform: scale(0.9);
filter: brightness(0.72);
}
}@import "tailwindcss";
@theme {
--color-stk01-paper: #eef0f4;
--color-stk01-paper: oklch(0.945 0.006 265);
--color-stk01-muted: #8a90a0;
--color-stk01-muted: oklch(0.64 0.018 265);
--color-stk01-a1: #6f6bf0;
--color-stk01-a1: oklch(0.62 0.17 274);
--color-stk01-a2: #8f5ce8;
--color-stk01-a2: oklch(0.55 0.16 292);
--color-stk01-b1: #3ec1d3;
--color-stk01-b1: oklch(0.66 0.15 200);
--color-stk01-b2: #4f8ee0;
--color-stk01-b2: oklch(0.60 0.16 250);
--color-stk01-c1: #4fd39a;
--color-stk01-c1: oklch(0.70 0.15 150);
--color-stk01-c2: #3bb7cf;
--color-stk01-c2: oklch(0.62 0.15 200);
--color-stk01-d1: #e9a53f;
--color-stk01-d1: oklch(0.72 0.16 60);
--color-stk01-d2: #e0713f;
--color-stk01-d2: oklch(0.64 0.17 30);
--font-stk01-sans: 'Archivo', system-ui, sans-serif;
--font-stk01-mono: 'JetBrains Mono', ui-monospace, monospace;
--animate-stk01-shrink: stk-01-shrink linear both;
}
/* Keyframes are a rule, not a value: there is no utility form for them, and the
@theme --animate-* entry above only names this block. */
@keyframes stk-01-shrink {
to {
transform: scale(0.9);
filter: brightness(0.72);
}
}<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=Archivo:wdth,wght@75..125,400;75..125,600;75..125,800&family=JetBrains+Mono:wght@400;500&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=Archivo:wdth,wght@75..125,400;75..125,600;75..125,800&family=JetBrains+Mono:wght@400;500&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. - Four
:nth-childsticky-offset rules collapse into one utility reading a per-card custom property:style="--i:2"plustop-[calc(90px+var(--i)*16px)]. - The four
:nth-childgradient rules disappear entirely — each card carries its ownbg-linear-[...], so recolouring one card touches one element. --animate-stk01-shrinkin@thememintsanimate-stk01-shrinkwith its timing baked in; only the@keyframesblock has to stay in CSS.animation-timelineandanimation-rangehave no utilities and should not —[animation-timeline:view()] [animation-range:exit]sits on the card next to the animation it drives.- The reduced-motion block becomes two variants in the markup:
motion-reduce:animate-noneon the cards andmotion-reduce:[scroll-behavior:auto]on the scroller. - Height stays fixed (
h-65= 260px) rather than fluid, because sticky offsets and a zero CLS score both need the box reserved up front.
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-Driven Scaling Card Stack
Source: https://codefronts.com/components/tailwind-stacked-cards/tailwind-scroll-driven-scaling-card-stack/
The storytelling hero, written in the markup. Each card is sticky with a stepped offset computed from its own --i, so top-[calc(90px+var(--i)*16px)] replaces four :nth-child rules. The scale-and-darken is an @theme animation — animate-stk01-shrink — driven by the two arbitrary properties Tailwind has no utility for, [animation-timeline:view()] and [animation-range:exit]. The only rules left in a stylesheet are the keyframes themselves.
## HTML
```html
<section class="stk-01 w-full min-h-svh h-svh overflow-y-auto bg-stk01-paper font-stk01-sans px-5 [scroll-behavior:smooth] motion-reduce:[scroll-behavior:auto]" aria-label="Scroll-driven scaling card stack">
<div class="w-[min(520px,100%)] mx-auto pt-[14vh] pb-[40vh]">
<p class="sticky top-4 z-10 m-0 text-center font-stk01-mono text-[11px] tracking-[0.16em] uppercase text-stk01-muted mb-[6vh]">Scroll ↓ inside this panel</p>
<article class="sticky top-[calc(90px+var(--i)*16px)] h-65 mb-15 rounded-[22px] p-[34px] text-white bg-linear-[135deg,var(--color-stk01-a1),var(--color-stk01-a2)] shadow-[0_30px_60px_-34px_rgba(20,24,50,0.7)] animate-stk01-shrink [animation-timeline:view()] [animation-range:exit] motion-reduce:animate-none" style="--i:0">
<span class="font-stk01-mono text-[13px] font-medium tracking-[0.2em] opacity-70">01</span>
<h3 class="mt-3.5 mb-2.5 text-3xl font-extrabold tracking-[-0.02em]">Capture</h3>
<p class="max-w-[34ch] text-base leading-relaxed opacity-90">Every idea starts as a rough note. We pin it before it escapes.</p>
</article>
<article class="sticky top-[calc(90px+var(--i)*16px)] h-65 mb-15 rounded-[22px] p-[34px] text-white bg-linear-[135deg,var(--color-stk01-b1),var(--color-stk01-b2)] shadow-[0_30px_60px_-34px_rgba(20,24,50,0.7)] animate-stk01-shrink [animation-timeline:view()] [animation-range:exit] motion-reduce:animate-none" style="--i:1">
<span class="font-stk01-mono text-[13px] font-medium tracking-[0.2em] opacity-70">02</span>
<h3 class="mt-3.5 mb-2.5 text-3xl font-extrabold tracking-[-0.02em]">Shape</h3>
<p class="max-w-[34ch] text-base leading-relaxed opacity-90">Notes compress into a plan — the earlier one settles quietly behind.</p>
</article>
<article class="sticky top-[calc(90px+var(--i)*16px)] h-65 mb-15 rounded-[22px] p-[34px] text-white bg-linear-[135deg,var(--color-stk01-c1),var(--color-stk01-c2)] shadow-[0_30px_60px_-34px_rgba(20,24,50,0.7)] animate-stk01-shrink [animation-timeline:view()] [animation-range:exit] motion-reduce:animate-none" style="--i:2">
<span class="font-stk01-mono text-[13px] font-medium tracking-[0.2em] opacity-70">03</span>
<h3 class="mt-3.5 mb-2.5 text-3xl font-extrabold tracking-[-0.02em]">Ship</h3>
<p class="max-w-[34ch] text-base leading-relaxed opacity-90">The plan surfaces on top, in focus, ready to hand off.</p>
</article>
<article class="sticky top-[calc(90px+var(--i)*16px)] h-65 mb-15 rounded-[22px] p-[34px] text-white bg-linear-[135deg,var(--color-stk01-d1),var(--color-stk01-d2)] shadow-[0_30px_60px_-34px_rgba(20,24,50,0.7)] animate-stk01-shrink [animation-timeline:view()] [animation-range:exit] motion-reduce:animate-none" style="--i:3">
<span class="font-stk01-mono text-[13px] font-medium tracking-[0.2em] opacity-70">04</span>
<h3 class="mt-3.5 mb-2.5 text-3xl font-extrabold tracking-[-0.02em]">Reflect</h3>
<p class="max-w-[34ch] text-base leading-relaxed opacity-90">What worked stacks up as depth you can scroll back through.</p>
</article>
<div class="pt-[12vh] text-center font-stk01-mono text-sm tracking-[0.14em] uppercase text-stk01-muted">Fin.</div>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-stk01-paper: #eef0f4;
--color-stk01-paper: oklch(0.945 0.006 265);
--color-stk01-muted: #8a90a0;
--color-stk01-muted: oklch(0.64 0.018 265);
--color-stk01-a1: #6f6bf0;
--color-stk01-a1: oklch(0.62 0.17 274);
--color-stk01-a2: #8f5ce8;
--color-stk01-a2: oklch(0.55 0.16 292);
--color-stk01-b1: #3ec1d3;
--color-stk01-b1: oklch(0.66 0.15 200);
--color-stk01-b2: #4f8ee0;
--color-stk01-b2: oklch(0.60 0.16 250);
--color-stk01-c1: #4fd39a;
--color-stk01-c1: oklch(0.70 0.15 150);
--color-stk01-c2: #3bb7cf;
--color-stk01-c2: oklch(0.62 0.15 200);
--color-stk01-d1: #e9a53f;
--color-stk01-d1: oklch(0.72 0.16 60);
--color-stk01-d2: #e0713f;
--color-stk01-d2: oklch(0.64 0.17 30);
--font-stk01-sans: 'Archivo', system-ui, sans-serif;
--font-stk01-mono: 'JetBrains Mono', ui-monospace, monospace;
--animate-stk01-shrink: stk-01-shrink linear both;
}
/* Keyframes are a rule, not a value: there is no utility form for them, and the
@theme --animate-* entry above only names this block. */
@keyframes stk-01-shrink {
to {
transform: scale(0.9);
filter: brightness(0.72);
}
}
```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-Driven Scaling Card Stack
Source: https://codefronts.com/components/tailwind-stacked-cards/tailwind-scroll-driven-scaling-card-stack/
The storytelling hero, written in the markup. Each card is sticky with a stepped offset computed from its own --i, so top-[calc(90px+var(--i)*16px)] replaces four :nth-child rules. The scale-and-darken is an @theme animation — animate-stk01-shrink — driven by the two arbitrary properties Tailwind has no utility for, [animation-timeline:view()] and [animation-range:exit]. The only rules left in a stylesheet are the keyframes themselves.
## HTML
```html
<section class="stk-01 w-full min-h-svh h-svh overflow-y-auto bg-stk01-paper font-stk01-sans px-5 [scroll-behavior:smooth] motion-reduce:[scroll-behavior:auto]" aria-label="Scroll-driven scaling card stack">
<div class="w-[min(520px,100%)] mx-auto pt-[14vh] pb-[40vh]">
<p class="sticky top-4 z-10 m-0 text-center font-stk01-mono text-[11px] tracking-[0.16em] uppercase text-stk01-muted mb-[6vh]">Scroll ↓ inside this panel</p>
<article class="sticky top-[calc(90px+var(--i)*16px)] h-65 mb-15 rounded-[22px] p-[34px] text-white bg-linear-[135deg,var(--color-stk01-a1),var(--color-stk01-a2)] shadow-[0_30px_60px_-34px_rgba(20,24,50,0.7)] animate-stk01-shrink [animation-timeline:view()] [animation-range:exit] motion-reduce:animate-none" style="--i:0">
<span class="font-stk01-mono text-[13px] font-medium tracking-[0.2em] opacity-70">01</span>
<h3 class="mt-3.5 mb-2.5 text-3xl font-extrabold tracking-[-0.02em]">Capture</h3>
<p class="max-w-[34ch] text-base leading-relaxed opacity-90">Every idea starts as a rough note. We pin it before it escapes.</p>
</article>
<article class="sticky top-[calc(90px+var(--i)*16px)] h-65 mb-15 rounded-[22px] p-[34px] text-white bg-linear-[135deg,var(--color-stk01-b1),var(--color-stk01-b2)] shadow-[0_30px_60px_-34px_rgba(20,24,50,0.7)] animate-stk01-shrink [animation-timeline:view()] [animation-range:exit] motion-reduce:animate-none" style="--i:1">
<span class="font-stk01-mono text-[13px] font-medium tracking-[0.2em] opacity-70">02</span>
<h3 class="mt-3.5 mb-2.5 text-3xl font-extrabold tracking-[-0.02em]">Shape</h3>
<p class="max-w-[34ch] text-base leading-relaxed opacity-90">Notes compress into a plan — the earlier one settles quietly behind.</p>
</article>
<article class="sticky top-[calc(90px+var(--i)*16px)] h-65 mb-15 rounded-[22px] p-[34px] text-white bg-linear-[135deg,var(--color-stk01-c1),var(--color-stk01-c2)] shadow-[0_30px_60px_-34px_rgba(20,24,50,0.7)] animate-stk01-shrink [animation-timeline:view()] [animation-range:exit] motion-reduce:animate-none" style="--i:2">
<span class="font-stk01-mono text-[13px] font-medium tracking-[0.2em] opacity-70">03</span>
<h3 class="mt-3.5 mb-2.5 text-3xl font-extrabold tracking-[-0.02em]">Ship</h3>
<p class="max-w-[34ch] text-base leading-relaxed opacity-90">The plan surfaces on top, in focus, ready to hand off.</p>
</article>
<article class="sticky top-[calc(90px+var(--i)*16px)] h-65 mb-15 rounded-[22px] p-[34px] text-white bg-linear-[135deg,var(--color-stk01-d1),var(--color-stk01-d2)] shadow-[0_30px_60px_-34px_rgba(20,24,50,0.7)] animate-stk01-shrink [animation-timeline:view()] [animation-range:exit] motion-reduce:animate-none" style="--i:3">
<span class="font-stk01-mono text-[13px] font-medium tracking-[0.2em] opacity-70">04</span>
<h3 class="mt-3.5 mb-2.5 text-3xl font-extrabold tracking-[-0.02em]">Reflect</h3>
<p class="max-w-[34ch] text-base leading-relaxed opacity-90">What worked stacks up as depth you can scroll back through.</p>
</article>
<div class="pt-[12vh] text-center font-stk01-mono text-sm tracking-[0.14em] uppercase text-stk01-muted">Fin.</div>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-stk01-paper: #eef0f4;
--color-stk01-paper: oklch(0.945 0.006 265);
--color-stk01-muted: #8a90a0;
--color-stk01-muted: oklch(0.64 0.018 265);
--color-stk01-a1: #6f6bf0;
--color-stk01-a1: oklch(0.62 0.17 274);
--color-stk01-a2: #8f5ce8;
--color-stk01-a2: oklch(0.55 0.16 292);
--color-stk01-b1: #3ec1d3;
--color-stk01-b1: oklch(0.66 0.15 200);
--color-stk01-b2: #4f8ee0;
--color-stk01-b2: oklch(0.60 0.16 250);
--color-stk01-c1: #4fd39a;
--color-stk01-c1: oklch(0.70 0.15 150);
--color-stk01-c2: #3bb7cf;
--color-stk01-c2: oklch(0.62 0.15 200);
--color-stk01-d1: #e9a53f;
--color-stk01-d1: oklch(0.72 0.16 60);
--color-stk01-d2: #e0713f;
--color-stk01-d2: oklch(0.64 0.17 30);
--font-stk01-sans: 'Archivo', system-ui, sans-serif;
--font-stk01-mono: 'JetBrains Mono', ui-monospace, monospace;
--animate-stk01-shrink: stk-01-shrink linear both;
}
/* Keyframes are a rule, not a value: there is no utility form for them, and the
@theme --animate-* entry above only names this block. */
@keyframes stk-01-shrink {
to {
transform: scale(0.9);
filter: brightness(0.72);
}
}
```How this works
The index goes on the element, not in a rule. A CSS stack needs one :nth-child block per card to step the sticky offset. In utilities you cannot write that from the child, so the number rides the element as a custom property and every derived utility reads it:
<article style="--i:2"
class="sticky top-[calc(90px+var(--i)*16px)] h-65 …">Add a fifth card and it takes its place with no re-indexing anywhere. This pattern recurs in more than half the collection, and it is the single biggest structural difference from the hand-written version.
The animation is a token. --animate-stk01-shrink: stk-01-shrink linear both in @theme gives a real animate-stk01-shrink utility that carries its own timing function and fill mode, so the element needs one class rather than four declarations. The @keyframes block stays in CSS — keyframes are a rule, not a value.
Scroll-driving stays arbitrary, correctly. animation-timeline and animation-range are not design-scale properties and Tailwind ships no scale for them. [animation-timeline:view()] [animation-range:exit] is the idiomatic answer, and it reads as exactly what it is: this animation's clock is the card's own trip through the viewport.
The fallback is free. Where animation-timeline is unsupported the animation simply never advances, so the card sits at its unanimated state and the reader gets a clean sticky stack. Nothing needs a feature query.
Make it yours
- Tune the compression by editing the
scale()andbrightness()end values in the keyframes — the one place this demo still needs CSS. - Change how much each pinned card peeks by editing the
16pxstep insidetop-[calc(90px+var(--i)*16px)]. - Swap
[animation-range:exit]for[animation-range:cover]to start the scale earlier in the card's travel. - Recolour a single card by editing its own
bg-linear-[...]utility; there is no colour map to keep in sync. - Add a card by copying one
<article>and incrementing its--i.
Gotchas — read before shipping
- Keep the fixed height (
h-65) on every card. Sticky math and CLS both depend on the box being reserved before paint. animation-timelineneeds Chrome/Edge 115+ or Safari 26+. Do not feature-query it — the unsupported path already degrades to a plain sticky stack.- Never animate
width/heightfor the shrink.animate-stk01-shrinkmovestransformandfilter, which stay on the compositor. - Arbitrary values cannot contain spaces.
top-[calc(90px+var(--i)*16px)]must be typed closed-up, or use the underscore escape. - The class string must be statically visible to the compiler.
top-[calc(90px+var(--i)*{{step}}px)]built at runtime produces nothing — keep the arithmetic in CSS and the varying part in the custom property. - The browser CDN build compiles in the page and is for prototyping only. Ship the CLI or a plugin build.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 115+ | 26+ | 128+ (sticky stack only) | 115+ |
Tailwind v4 itself needs Chrome 111+, Safari 16.4+, Firefox 128+. position:sticky is universal, so the stack always reads; the scale-and-darken is a progressive enhancement on animation-timeline:view() and simply does not run where that is missing.