02 / 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 Swipe Card Deck Drag to Dismiss
The decision deck, ported to variants. The depth of the pile stops being three :nth-last-child rules and becomes nth-last-2:, nth-last-3: and nth-last-[n+4]: typed straight onto the card. The fly-off is a data-gone attribute the script sets, with the two exit transforms written as important arbitrary utilities so they beat the inline transform the drag leaves behind. The LIKE / NOPE stamps become real spans driven by group-data-[hint=yes]:, not content strings in a stylesheet.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="stk-02 w-full min-h-svh flex flex-col items-center justify-center gap-[22px] px-5 py-10 font-stk02-sans bg-stk02-bg [background-image:radial-gradient(circle_at_50%_0%,var(--color-stk02-glow),transparent_70%)]" aria-label="Swipeable card deck">
<div class="relative w-[min(320px,86vw)] h-105" id="stk-02-deck">
<p class="absolute inset-0 grid place-items-center p-8 text-center text-base text-stk02-muted">That’s everyone for now.</p>
<article class="group absolute inset-0 rounded-[22px] overflow-hidden bg-stk02-card select-none cursor-grab active:cursor-grabbing [touch-action:none] shadow-[0_24px_50px_-24px_rgba(0,0,0,0.75)] transition-[transform,opacity] duration-500 ease-stk02 nth-last-2:scale-[0.94] nth-last-2:translate-y-4 nth-last-3:scale-[0.88] nth-last-3:translate-y-8 nth-last-[n+4]:scale-[0.82] nth-last-[n+4]:translate-y-[46px] nth-last-[n+4]:opacity-0 data-[dragging=true]:transition-none data-[gone]:opacity-0 data-[gone=yes]:[transform:translate(140%,-10%)_rotate(22deg)]! data-[gone=no]:[transform:translate(-140%,-10%)_rotate(-22deg)]! motion-reduce:transition-[opacity] motion-reduce:duration-200 focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-3" tabindex="0">
<img class="block w-full h-[74%] object-cover pointer-events-none" src="https://picsum.photos/seed/tw-swipe-1/400/460" width="400" height="460" alt="Mountain lake at first light">
<div class="px-[18px] py-4 text-white">
<h3 class="text-xl font-extrabold tracking-[-0.02em]">Aria, 27</h3>
<p class="mt-0.5 text-[13px] text-stk02-muted">Trail runner · Portland</p>
</div>
<span class="absolute top-[22px] left-[18px] rotate-[-14deg] rounded-lg border-[3px] border-stk02-yes px-3 py-1.5 font-stk02-mono text-[15px] font-bold tracking-[0.08em] text-stk02-yes opacity-0 transition-opacity duration-150 group-data-[hint=yes]:opacity-100" aria-hidden="true">LIKE</span>
<span class="absolute top-[22px] right-[18px] rotate-[14deg] rounded-lg border-[3px] border-stk02-no px-3 py-1.5 font-stk02-mono text-[15px] font-bold tracking-[0.08em] text-stk02-no opacity-0 transition-opacity duration-150 group-data-[hint=no]:opacity-100" aria-hidden="true">NOPE</span>
</article>
<article class="group absolute inset-0 rounded-[22px] overflow-hidden bg-stk02-card select-none cursor-grab active:cursor-grabbing [touch-action:none] shadow-[0_24px_50px_-24px_rgba(0,0,0,0.75)] transition-[transform,opacity] duration-500 ease-stk02 nth-last-2:scale-[0.94] nth-last-2:translate-y-4 nth-last-3:scale-[0.88] nth-last-3:translate-y-8 nth-last-[n+4]:scale-[0.82] nth-last-[n+4]:translate-y-[46px] nth-last-[n+4]:opacity-0 data-[dragging=true]:transition-none data-[gone]:opacity-0 data-[gone=yes]:[transform:translate(140%,-10%)_rotate(22deg)]! data-[gone=no]:[transform:translate(-140%,-10%)_rotate(-22deg)]! motion-reduce:transition-[opacity] motion-reduce:duration-200 focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-3" tabindex="0">
<img class="block w-full h-[74%] object-cover pointer-events-none" src="https://picsum.photos/seed/tw-swipe-2/400/460" width="400" height="460" alt="City rooftop in the evening">
<div class="px-[18px] py-4 text-white">
<h3 class="text-xl font-extrabold tracking-[-0.02em]">Devon, 31</h3>
<p class="mt-0.5 text-[13px] text-stk02-muted">Coffee nerd · Austin</p>
</div>
<span class="absolute top-[22px] left-[18px] rotate-[-14deg] rounded-lg border-[3px] border-stk02-yes px-3 py-1.5 font-stk02-mono text-[15px] font-bold tracking-[0.08em] text-stk02-yes opacity-0 transition-opacity duration-150 group-data-[hint=yes]:opacity-100" aria-hidden="true">LIKE</span>
<span class="absolute top-[22px] right-[18px] rotate-[14deg] rounded-lg border-[3px] border-stk02-no px-3 py-1.5 font-stk02-mono text-[15px] font-bold tracking-[0.08em] text-stk02-no opacity-0 transition-opacity duration-150 group-data-[hint=no]:opacity-100" aria-hidden="true">NOPE</span>
</article>
<article class="group absolute inset-0 rounded-[22px] overflow-hidden bg-stk02-card select-none cursor-grab active:cursor-grabbing [touch-action:none] shadow-[0_24px_50px_-24px_rgba(0,0,0,0.75)] transition-[transform,opacity] duration-500 ease-stk02 nth-last-2:scale-[0.94] nth-last-2:translate-y-4 nth-last-3:scale-[0.88] nth-last-3:translate-y-8 nth-last-[n+4]:scale-[0.82] nth-last-[n+4]:translate-y-[46px] nth-last-[n+4]:opacity-0 data-[dragging=true]:transition-none data-[gone]:opacity-0 data-[gone=yes]:[transform:translate(140%,-10%)_rotate(22deg)]! data-[gone=no]:[transform:translate(-140%,-10%)_rotate(-22deg)]! motion-reduce:transition-[opacity] motion-reduce:duration-200 focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-3" tabindex="0">
<img class="block w-full h-[74%] object-cover pointer-events-none" src="https://picsum.photos/seed/tw-swipe-3/400/460" width="400" height="460" alt="Empty desert road">
<div class="px-[18px] py-4 text-white">
<h3 class="text-xl font-extrabold tracking-[-0.02em]">Sky, 24</h3>
<p class="mt-0.5 text-[13px] text-stk02-muted">Van-lifer · Denver</p>
</div>
<span class="absolute top-[22px] left-[18px] rotate-[-14deg] rounded-lg border-[3px] border-stk02-yes px-3 py-1.5 font-stk02-mono text-[15px] font-bold tracking-[0.08em] text-stk02-yes opacity-0 transition-opacity duration-150 group-data-[hint=yes]:opacity-100" aria-hidden="true">LIKE</span>
<span class="absolute top-[22px] right-[18px] rotate-[14deg] rounded-lg border-[3px] border-stk02-no px-3 py-1.5 font-stk02-mono text-[15px] font-bold tracking-[0.08em] text-stk02-no opacity-0 transition-opacity duration-150 group-data-[hint=no]:opacity-100" aria-hidden="true">NOPE</span>
</article>
<article class="group absolute inset-0 rounded-[22px] overflow-hidden bg-stk02-card select-none cursor-grab active:cursor-grabbing [touch-action:none] shadow-[0_24px_50px_-24px_rgba(0,0,0,0.75)] transition-[transform,opacity] duration-500 ease-stk02 nth-last-2:scale-[0.94] nth-last-2:translate-y-4 nth-last-3:scale-[0.88] nth-last-3:translate-y-8 nth-last-[n+4]:scale-[0.82] nth-last-[n+4]:translate-y-[46px] nth-last-[n+4]:opacity-0 data-[dragging=true]:transition-none data-[gone]:opacity-0 data-[gone=yes]:[transform:translate(140%,-10%)_rotate(22deg)]! data-[gone=no]:[transform:translate(-140%,-10%)_rotate(-22deg)]! motion-reduce:transition-[opacity] motion-reduce:duration-200 focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-3" tabindex="0">
<img class="block w-full h-[74%] object-cover pointer-events-none" src="https://picsum.photos/seed/tw-swipe-4/400/460" width="400" height="460" alt="Forest path under tall pines">
<div class="px-[18px] py-4 text-white">
<h3 class="text-xl font-extrabold tracking-[-0.02em]">Rowan, 29</h3>
<p class="mt-0.5 text-[13px] text-stk02-muted">Botanist · Seattle</p>
</div>
<span class="absolute top-[22px] left-[18px] rotate-[-14deg] rounded-lg border-[3px] border-stk02-yes px-3 py-1.5 font-stk02-mono text-[15px] font-bold tracking-[0.08em] text-stk02-yes opacity-0 transition-opacity duration-150 group-data-[hint=yes]:opacity-100" aria-hidden="true">LIKE</span>
<span class="absolute top-[22px] right-[18px] rotate-[14deg] rounded-lg border-[3px] border-stk02-no px-3 py-1.5 font-stk02-mono text-[15px] font-bold tracking-[0.08em] text-stk02-no opacity-0 transition-opacity duration-150 group-data-[hint=no]:opacity-100" aria-hidden="true">NOPE</span>
</article>
</div>
<div class="flex gap-6">
<button class="grid place-items-center size-15 rounded-full border-0 bg-stk02-no text-2xl text-white cursor-pointer shadow-[0_10px_24px_-10px_rgba(0,0,0,0.6)] transition-transform duration-150 hover:-translate-y-[3px] hover:scale-105 active:scale-95 focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-3" id="stk-02-no" type="button" aria-label="Pass on this card">×</button>
<button class="grid place-items-center size-15 rounded-full border-0 bg-stk02-yes text-2xl text-white cursor-pointer shadow-[0_10px_24px_-10px_rgba(0,0,0,0.6)] transition-transform duration-150 hover:-translate-y-[3px] hover:scale-105 active:scale-95 focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-3" id="stk-02-yes" type="button" aria-label="Like this card">♥</button>
</div>
<p class="m-0 font-stk02-mono text-[11px] tracking-[0.14em] uppercase text-stk02-muted" id="stk-02-count" aria-live="polite"></p>
</section><section class="stk-02 w-full min-h-svh flex flex-col items-center justify-center gap-[22px] px-5 py-10 font-stk02-sans bg-stk02-bg [background-image:radial-gradient(circle_at_50%_0%,var(--color-stk02-glow),transparent_70%)]" aria-label="Swipeable card deck">
<div class="relative w-[min(320px,86vw)] h-105" id="stk-02-deck">
<p class="absolute inset-0 grid place-items-center p-8 text-center text-base text-stk02-muted">That’s everyone for now.</p>
<article class="group absolute inset-0 rounded-[22px] overflow-hidden bg-stk02-card select-none cursor-grab active:cursor-grabbing [touch-action:none] shadow-[0_24px_50px_-24px_rgba(0,0,0,0.75)] transition-[transform,opacity] duration-500 ease-stk02 nth-last-2:scale-[0.94] nth-last-2:translate-y-4 nth-last-3:scale-[0.88] nth-last-3:translate-y-8 nth-last-[n+4]:scale-[0.82] nth-last-[n+4]:translate-y-[46px] nth-last-[n+4]:opacity-0 data-[dragging=true]:transition-none data-[gone]:opacity-0 data-[gone=yes]:[transform:translate(140%,-10%)_rotate(22deg)]! data-[gone=no]:[transform:translate(-140%,-10%)_rotate(-22deg)]! motion-reduce:transition-[opacity] motion-reduce:duration-200 focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-3" tabindex="0">
<img class="block w-full h-[74%] object-cover pointer-events-none" src="https://picsum.photos/seed/tw-swipe-1/400/460" width="400" height="460" alt="Mountain lake at first light">
<div class="px-[18px] py-4 text-white">
<h3 class="text-xl font-extrabold tracking-[-0.02em]">Aria, 27</h3>
<p class="mt-0.5 text-[13px] text-stk02-muted">Trail runner · Portland</p>
</div>
<span class="absolute top-[22px] left-[18px] rotate-[-14deg] rounded-lg border-[3px] border-stk02-yes px-3 py-1.5 font-stk02-mono text-[15px] font-bold tracking-[0.08em] text-stk02-yes opacity-0 transition-opacity duration-150 group-data-[hint=yes]:opacity-100" aria-hidden="true">LIKE</span>
<span class="absolute top-[22px] right-[18px] rotate-[14deg] rounded-lg border-[3px] border-stk02-no px-3 py-1.5 font-stk02-mono text-[15px] font-bold tracking-[0.08em] text-stk02-no opacity-0 transition-opacity duration-150 group-data-[hint=no]:opacity-100" aria-hidden="true">NOPE</span>
</article>
<article class="group absolute inset-0 rounded-[22px] overflow-hidden bg-stk02-card select-none cursor-grab active:cursor-grabbing [touch-action:none] shadow-[0_24px_50px_-24px_rgba(0,0,0,0.75)] transition-[transform,opacity] duration-500 ease-stk02 nth-last-2:scale-[0.94] nth-last-2:translate-y-4 nth-last-3:scale-[0.88] nth-last-3:translate-y-8 nth-last-[n+4]:scale-[0.82] nth-last-[n+4]:translate-y-[46px] nth-last-[n+4]:opacity-0 data-[dragging=true]:transition-none data-[gone]:opacity-0 data-[gone=yes]:[transform:translate(140%,-10%)_rotate(22deg)]! data-[gone=no]:[transform:translate(-140%,-10%)_rotate(-22deg)]! motion-reduce:transition-[opacity] motion-reduce:duration-200 focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-3" tabindex="0">
<img class="block w-full h-[74%] object-cover pointer-events-none" src="https://picsum.photos/seed/tw-swipe-2/400/460" width="400" height="460" alt="City rooftop in the evening">
<div class="px-[18px] py-4 text-white">
<h3 class="text-xl font-extrabold tracking-[-0.02em]">Devon, 31</h3>
<p class="mt-0.5 text-[13px] text-stk02-muted">Coffee nerd · Austin</p>
</div>
<span class="absolute top-[22px] left-[18px] rotate-[-14deg] rounded-lg border-[3px] border-stk02-yes px-3 py-1.5 font-stk02-mono text-[15px] font-bold tracking-[0.08em] text-stk02-yes opacity-0 transition-opacity duration-150 group-data-[hint=yes]:opacity-100" aria-hidden="true">LIKE</span>
<span class="absolute top-[22px] right-[18px] rotate-[14deg] rounded-lg border-[3px] border-stk02-no px-3 py-1.5 font-stk02-mono text-[15px] font-bold tracking-[0.08em] text-stk02-no opacity-0 transition-opacity duration-150 group-data-[hint=no]:opacity-100" aria-hidden="true">NOPE</span>
</article>
<article class="group absolute inset-0 rounded-[22px] overflow-hidden bg-stk02-card select-none cursor-grab active:cursor-grabbing [touch-action:none] shadow-[0_24px_50px_-24px_rgba(0,0,0,0.75)] transition-[transform,opacity] duration-500 ease-stk02 nth-last-2:scale-[0.94] nth-last-2:translate-y-4 nth-last-3:scale-[0.88] nth-last-3:translate-y-8 nth-last-[n+4]:scale-[0.82] nth-last-[n+4]:translate-y-[46px] nth-last-[n+4]:opacity-0 data-[dragging=true]:transition-none data-[gone]:opacity-0 data-[gone=yes]:[transform:translate(140%,-10%)_rotate(22deg)]! data-[gone=no]:[transform:translate(-140%,-10%)_rotate(-22deg)]! motion-reduce:transition-[opacity] motion-reduce:duration-200 focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-3" tabindex="0">
<img class="block w-full h-[74%] object-cover pointer-events-none" src="https://picsum.photos/seed/tw-swipe-3/400/460" width="400" height="460" alt="Empty desert road">
<div class="px-[18px] py-4 text-white">
<h3 class="text-xl font-extrabold tracking-[-0.02em]">Sky, 24</h3>
<p class="mt-0.5 text-[13px] text-stk02-muted">Van-lifer · Denver</p>
</div>
<span class="absolute top-[22px] left-[18px] rotate-[-14deg] rounded-lg border-[3px] border-stk02-yes px-3 py-1.5 font-stk02-mono text-[15px] font-bold tracking-[0.08em] text-stk02-yes opacity-0 transition-opacity duration-150 group-data-[hint=yes]:opacity-100" aria-hidden="true">LIKE</span>
<span class="absolute top-[22px] right-[18px] rotate-[14deg] rounded-lg border-[3px] border-stk02-no px-3 py-1.5 font-stk02-mono text-[15px] font-bold tracking-[0.08em] text-stk02-no opacity-0 transition-opacity duration-150 group-data-[hint=no]:opacity-100" aria-hidden="true">NOPE</span>
</article>
<article class="group absolute inset-0 rounded-[22px] overflow-hidden bg-stk02-card select-none cursor-grab active:cursor-grabbing [touch-action:none] shadow-[0_24px_50px_-24px_rgba(0,0,0,0.75)] transition-[transform,opacity] duration-500 ease-stk02 nth-last-2:scale-[0.94] nth-last-2:translate-y-4 nth-last-3:scale-[0.88] nth-last-3:translate-y-8 nth-last-[n+4]:scale-[0.82] nth-last-[n+4]:translate-y-[46px] nth-last-[n+4]:opacity-0 data-[dragging=true]:transition-none data-[gone]:opacity-0 data-[gone=yes]:[transform:translate(140%,-10%)_rotate(22deg)]! data-[gone=no]:[transform:translate(-140%,-10%)_rotate(-22deg)]! motion-reduce:transition-[opacity] motion-reduce:duration-200 focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-3" tabindex="0">
<img class="block w-full h-[74%] object-cover pointer-events-none" src="https://picsum.photos/seed/tw-swipe-4/400/460" width="400" height="460" alt="Forest path under tall pines">
<div class="px-[18px] py-4 text-white">
<h3 class="text-xl font-extrabold tracking-[-0.02em]">Rowan, 29</h3>
<p class="mt-0.5 text-[13px] text-stk02-muted">Botanist · Seattle</p>
</div>
<span class="absolute top-[22px] left-[18px] rotate-[-14deg] rounded-lg border-[3px] border-stk02-yes px-3 py-1.5 font-stk02-mono text-[15px] font-bold tracking-[0.08em] text-stk02-yes opacity-0 transition-opacity duration-150 group-data-[hint=yes]:opacity-100" aria-hidden="true">LIKE</span>
<span class="absolute top-[22px] right-[18px] rotate-[14deg] rounded-lg border-[3px] border-stk02-no px-3 py-1.5 font-stk02-mono text-[15px] font-bold tracking-[0.08em] text-stk02-no opacity-0 transition-opacity duration-150 group-data-[hint=no]:opacity-100" aria-hidden="true">NOPE</span>
</article>
</div>
<div class="flex gap-6">
<button class="grid place-items-center size-15 rounded-full border-0 bg-stk02-no text-2xl text-white cursor-pointer shadow-[0_10px_24px_-10px_rgba(0,0,0,0.6)] transition-transform duration-150 hover:-translate-y-[3px] hover:scale-105 active:scale-95 focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-3" id="stk-02-no" type="button" aria-label="Pass on this card">×</button>
<button class="grid place-items-center size-15 rounded-full border-0 bg-stk02-yes text-2xl text-white cursor-pointer shadow-[0_10px_24px_-10px_rgba(0,0,0,0.6)] transition-transform duration-150 hover:-translate-y-[3px] hover:scale-105 active:scale-95 focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-3" id="stk-02-yes" type="button" aria-label="Like this card">♥</button>
</div>
<p class="m-0 font-stk02-mono text-[11px] tracking-[0.14em] uppercase text-stk02-muted" id="stk-02-count" aria-live="polite"></p>
</section>@import "tailwindcss";
@theme {
--color-stk02-bg: #15121f;
--color-stk02-bg: oklch(0.19 0.025 295);
--color-stk02-glow: #2a2340;
--color-stk02-glow: oklch(0.28 0.045 295);
--color-stk02-card: #241f36;
--color-stk02-card: oklch(0.26 0.04 295);
--color-stk02-muted: #c3bdd8;
--color-stk02-muted: oklch(0.81 0.03 295);
--color-stk02-yes: #45c98a;
--color-stk02-yes: oklch(0.70 0.17 150);
--color-stk02-no: #e2604a;
--color-stk02-no: oklch(0.65 0.20 20);
--font-stk02-sans: 'Archivo', system-ui, sans-serif;
--font-stk02-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-stk02: cubic-bezier(0.2, 0.9, 0.2, 1);
}@import "tailwindcss";
@theme {
--color-stk02-bg: #15121f;
--color-stk02-bg: oklch(0.19 0.025 295);
--color-stk02-glow: #2a2340;
--color-stk02-glow: oklch(0.28 0.045 295);
--color-stk02-card: #241f36;
--color-stk02-card: oklch(0.26 0.04 295);
--color-stk02-muted: #c3bdd8;
--color-stk02-muted: oklch(0.81 0.03 295);
--color-stk02-yes: #45c98a;
--color-stk02-yes: oklch(0.70 0.17 150);
--color-stk02-no: #e2604a;
--color-stk02-no: oklch(0.65 0.20 20);
--font-stk02-sans: 'Archivo', system-ui, sans-serif;
--font-stk02-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-stk02: cubic-bezier(0.2, 0.9, 0.2, 1);
}(() => {
const root = document.querySelector('.stk-02');
if (!root) return;
const deck = root.querySelector('#stk-02-deck');
const countEl = root.querySelector('#stk-02-count');
const THRESHOLD = 90;
const cards = () => [...deck.querySelectorAll('article')];
const top = () => cards()[cards().length - 1] || null;
function refresh() {
const t = top();
if (t) bind(t);
const n = cards().length;
countEl.textContent = n ? n + ' card' + (n > 1 ? 's' : '') + ' left' : 'Deck empty — reload to reshuffle';
}
/* State only: the exit transform is a data-[gone] utility in the markup, so the
destination stays visible in the class list rather than hidden in a script. */
function fling(card, dir) {
card.style.transform = '';
card.dataset.gone = dir > 0 ? 'yes' : 'no';
card.addEventListener('transitionend', () => { card.remove(); refresh(); }, { once: true });
}
function bind(card) {
if (card.dataset.bound) return;
card.dataset.bound = '1';
let startX = 0, dx = 0, dragging = false;
card.addEventListener('pointerdown', (e) => {
if (card !== top()) return;
dragging = true; startX = e.clientX;
card.setPointerCapture(e.pointerId);
card.dataset.dragging = 'true';
});
/* The only inline style in the demo, and it has to be: this value is the
pointer's live position and cannot exist as a static class. */
card.addEventListener('pointermove', (e) => {
if (!dragging) return;
dx = e.clientX - startX;
card.style.transform = 'translateX(' + dx + 'px) rotate(' + (dx / 18) + 'deg)';
if (dx > 30) card.dataset.hint = 'yes';
else if (dx < -30) card.dataset.hint = 'no';
else delete card.dataset.hint;
});
card.addEventListener('pointerup', () => {
if (!dragging) return;
dragging = false;
delete card.dataset.dragging;
if (Math.abs(dx) > THRESHOLD) { fling(card, dx > 0 ? 1 : -1); }
else { card.style.transform = ''; delete card.dataset.hint; }
dx = 0;
});
card.addEventListener('keydown', (e) => {
if (e.key === 'ArrowRight') fling(card, 1);
if (e.key === 'ArrowLeft') fling(card, -1);
});
}
root.querySelector('#stk-02-yes').addEventListener('click', () => { const t = top(); if (t) fling(t, 1); });
root.querySelector('#stk-02-no').addEventListener('click', () => { const t = top(); if (t) fling(t, -1); });
refresh();
})();(() => {
const root = document.querySelector('.stk-02');
if (!root) return;
const deck = root.querySelector('#stk-02-deck');
const countEl = root.querySelector('#stk-02-count');
const THRESHOLD = 90;
const cards = () => [...deck.querySelectorAll('article')];
const top = () => cards()[cards().length - 1] || null;
function refresh() {
const t = top();
if (t) bind(t);
const n = cards().length;
countEl.textContent = n ? n + ' card' + (n > 1 ? 's' : '') + ' left' : 'Deck empty — reload to reshuffle';
}
/* State only: the exit transform is a data-[gone] utility in the markup, so the
destination stays visible in the class list rather than hidden in a script. */
function fling(card, dir) {
card.style.transform = '';
card.dataset.gone = dir > 0 ? 'yes' : 'no';
card.addEventListener('transitionend', () => { card.remove(); refresh(); }, { once: true });
}
function bind(card) {
if (card.dataset.bound) return;
card.dataset.bound = '1';
let startX = 0, dx = 0, dragging = false;
card.addEventListener('pointerdown', (e) => {
if (card !== top()) return;
dragging = true; startX = e.clientX;
card.setPointerCapture(e.pointerId);
card.dataset.dragging = 'true';
});
/* The only inline style in the demo, and it has to be: this value is the
pointer's live position and cannot exist as a static class. */
card.addEventListener('pointermove', (e) => {
if (!dragging) return;
dx = e.clientX - startX;
card.style.transform = 'translateX(' + dx + 'px) rotate(' + (dx / 18) + 'deg)';
if (dx > 30) card.dataset.hint = 'yes';
else if (dx < -30) card.dataset.hint = 'no';
else delete card.dataset.hint;
});
card.addEventListener('pointerup', () => {
if (!dragging) return;
dragging = false;
delete card.dataset.dragging;
if (Math.abs(dx) > THRESHOLD) { fling(card, dx > 0 ? 1 : -1); }
else { card.style.transform = ''; delete card.dataset.hint; }
dx = 0;
});
card.addEventListener('keydown', (e) => {
if (e.key === 'ArrowRight') fling(card, 1);
if (e.key === 'ArrowLeft') fling(card, -1);
});
}
root.querySelector('#stk-02-yes').addEventListener('click', () => { const t = top(); if (t) fling(t, 1); });
root.querySelector('#stk-02-no').addEventListener('click', () => { const t = top(); if (t) fling(t, -1); });
refresh();
})();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-stk02-bg: #15121f;
--color-stk02-bg: oklch(0.19 0.025 295);
--color-stk02-glow: #2a2340;
--color-stk02-glow: oklch(0.28 0.045 295);
--color-stk02-card: #241f36;
--color-stk02-card: oklch(0.26 0.04 295);
--color-stk02-muted: #c3bdd8;
--color-stk02-muted: oklch(0.81 0.03 295);
--color-stk02-yes: #45c98a;
--color-stk02-yes: oklch(0.70 0.17 150);
--color-stk02-no: #e2604a;
--color-stk02-no: oklch(0.65 0.20 20);
--font-stk02-sans: 'Archivo', system-ui, sans-serif;
--font-stk02-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-stk02: cubic-bezier(0.2, 0.9, 0.2, 1);
}@import "tailwindcss";
@theme {
--color-stk02-bg: #15121f;
--color-stk02-bg: oklch(0.19 0.025 295);
--color-stk02-glow: #2a2340;
--color-stk02-glow: oklch(0.28 0.045 295);
--color-stk02-card: #241f36;
--color-stk02-card: oklch(0.26 0.04 295);
--color-stk02-muted: #c3bdd8;
--color-stk02-muted: oklch(0.81 0.03 295);
--color-stk02-yes: #45c98a;
--color-stk02-yes: oklch(0.70 0.17 150);
--color-stk02-no: #e2604a;
--color-stk02-no: oklch(0.65 0.20 20);
--font-stk02-sans: 'Archivo', system-ui, sans-serif;
--font-stk02-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-stk02: cubic-bezier(0.2, 0.9, 0.2, 1);
}<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. - The three
:nth-last-childdepth rules becomenth-last-2:,nth-last-3:andnth-last-[n+4]:on the card itself — the pile's whole geometry is now readable in one class list. - The fly-off moves out of the script and into
data-[gone=yes]:[transform:…]!; the!is required because the drag leaves an inlinetransformthat would otherwise win. - The LIKE / NOPE
::before/::aftercontentrules become realaria-hiddenspans revealed bygroup-data-[hint=yes]:opacity-100. touch-actionhas no utility and stays arbitrary —[touch-action:none]next tocursor-grab, which is where a reader looking for gesture behaviour will look.- This stylesheet is only
@theme. Every selector in the CSS version had a variant equivalent, which is unusual for a JS-driven demo and worth noticing. - The script sets attributes and removes nodes; it never adds a class and never reads one, so the visual contract stays entirely in the markup.
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 Swipe Card Deck Drag to Dismiss
Source: https://codefronts.com/components/tailwind-stacked-cards/tailwind-swipe-card-deck-drag-to-dismiss/
The decision deck, ported to variants. The depth of the pile stops being three :nth-last-child rules and becomes nth-last-2:, nth-last-3: and nth-last-[n+4]: typed straight onto the card. The fly-off is a data-gone attribute the script sets, with the two exit transforms written as important arbitrary utilities so they beat the inline transform the drag leaves behind. The LIKE / NOPE stamps become real spans driven by group-data-[hint=yes]:, not content strings in a stylesheet.
## HTML
```html
<section class="stk-02 w-full min-h-svh flex flex-col items-center justify-center gap-[22px] px-5 py-10 font-stk02-sans bg-stk02-bg [background-image:radial-gradient(circle_at_50%_0%,var(--color-stk02-glow),transparent_70%)]" aria-label="Swipeable card deck">
<div class="relative w-[min(320px,86vw)] h-105" id="stk-02-deck">
<p class="absolute inset-0 grid place-items-center p-8 text-center text-base text-stk02-muted">That’s everyone for now.</p>
<article class="group absolute inset-0 rounded-[22px] overflow-hidden bg-stk02-card select-none cursor-grab active:cursor-grabbing [touch-action:none] shadow-[0_24px_50px_-24px_rgba(0,0,0,0.75)] transition-[transform,opacity] duration-500 ease-stk02 nth-last-2:scale-[0.94] nth-last-2:translate-y-4 nth-last-3:scale-[0.88] nth-last-3:translate-y-8 nth-last-[n+4]:scale-[0.82] nth-last-[n+4]:translate-y-[46px] nth-last-[n+4]:opacity-0 data-[dragging=true]:transition-none data-[gone]:opacity-0 data-[gone=yes]:[transform:translate(140%,-10%)_rotate(22deg)]! data-[gone=no]:[transform:translate(-140%,-10%)_rotate(-22deg)]! motion-reduce:transition-[opacity] motion-reduce:duration-200 focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-3" tabindex="0">
<img class="block w-full h-[74%] object-cover pointer-events-none" src="https://picsum.photos/seed/tw-swipe-1/400/460" width="400" height="460" alt="Mountain lake at first light">
<div class="px-[18px] py-4 text-white">
<h3 class="text-xl font-extrabold tracking-[-0.02em]">Aria, 27</h3>
<p class="mt-0.5 text-[13px] text-stk02-muted">Trail runner · Portland</p>
</div>
<span class="absolute top-[22px] left-[18px] rotate-[-14deg] rounded-lg border-[3px] border-stk02-yes px-3 py-1.5 font-stk02-mono text-[15px] font-bold tracking-[0.08em] text-stk02-yes opacity-0 transition-opacity duration-150 group-data-[hint=yes]:opacity-100" aria-hidden="true">LIKE</span>
<span class="absolute top-[22px] right-[18px] rotate-[14deg] rounded-lg border-[3px] border-stk02-no px-3 py-1.5 font-stk02-mono text-[15px] font-bold tracking-[0.08em] text-stk02-no opacity-0 transition-opacity duration-150 group-data-[hint=no]:opacity-100" aria-hidden="true">NOPE</span>
</article>
<article class="group absolute inset-0 rounded-[22px] overflow-hidden bg-stk02-card select-none cursor-grab active:cursor-grabbing [touch-action:none] shadow-[0_24px_50px_-24px_rgba(0,0,0,0.75)] transition-[transform,opacity] duration-500 ease-stk02 nth-last-2:scale-[0.94] nth-last-2:translate-y-4 nth-last-3:scale-[0.88] nth-last-3:translate-y-8 nth-last-[n+4]:scale-[0.82] nth-last-[n+4]:translate-y-[46px] nth-last-[n+4]:opacity-0 data-[dragging=true]:transition-none data-[gone]:opacity-0 data-[gone=yes]:[transform:translate(140%,-10%)_rotate(22deg)]! data-[gone=no]:[transform:translate(-140%,-10%)_rotate(-22deg)]! motion-reduce:transition-[opacity] motion-reduce:duration-200 focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-3" tabindex="0">
<img class="block w-full h-[74%] object-cover pointer-events-none" src="https://picsum.photos/seed/tw-swipe-2/400/460" width="400" height="460" alt="City rooftop in the evening">
<div class="px-[18px] py-4 text-white">
<h3 class="text-xl font-extrabold tracking-[-0.02em]">Devon, 31</h3>
<p class="mt-0.5 text-[13px] text-stk02-muted">Coffee nerd · Austin</p>
</div>
<span class="absolute top-[22px] left-[18px] rotate-[-14deg] rounded-lg border-[3px] border-stk02-yes px-3 py-1.5 font-stk02-mono text-[15px] font-bold tracking-[0.08em] text-stk02-yes opacity-0 transition-opacity duration-150 group-data-[hint=yes]:opacity-100" aria-hidden="true">LIKE</span>
<span class="absolute top-[22px] right-[18px] rotate-[14deg] rounded-lg border-[3px] border-stk02-no px-3 py-1.5 font-stk02-mono text-[15px] font-bold tracking-[0.08em] text-stk02-no opacity-0 transition-opacity duration-150 group-data-[hint=no]:opacity-100" aria-hidden="true">NOPE</span>
</article>
<article class="group absolute inset-0 rounded-[22px] overflow-hidden bg-stk02-card select-none cursor-grab active:cursor-grabbing [touch-action:none] shadow-[0_24px_50px_-24px_rgba(0,0,0,0.75)] transition-[transform,opacity] duration-500 ease-stk02 nth-last-2:scale-[0.94] nth-last-2:translate-y-4 nth-last-3:scale-[0.88] nth-last-3:translate-y-8 nth-last-[n+4]:scale-[0.82] nth-last-[n+4]:translate-y-[46px] nth-last-[n+4]:opacity-0 data-[dragging=true]:transition-none data-[gone]:opacity-0 data-[gone=yes]:[transform:translate(140%,-10%)_rotate(22deg)]! data-[gone=no]:[transform:translate(-140%,-10%)_rotate(-22deg)]! motion-reduce:transition-[opacity] motion-reduce:duration-200 focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-3" tabindex="0">
<img class="block w-full h-[74%] object-cover pointer-events-none" src="https://picsum.photos/seed/tw-swipe-3/400/460" width="400" height="460" alt="Empty desert road">
<div class="px-[18px] py-4 text-white">
<h3 class="text-xl font-extrabold tracking-[-0.02em]">Sky, 24</h3>
<p class="mt-0.5 text-[13px] text-stk02-muted">Van-lifer · Denver</p>
</div>
<span class="absolute top-[22px] left-[18px] rotate-[-14deg] rounded-lg border-[3px] border-stk02-yes px-3 py-1.5 font-stk02-mono text-[15px] font-bold tracking-[0.08em] text-stk02-yes opacity-0 transition-opacity duration-150 group-data-[hint=yes]:opacity-100" aria-hidden="true">LIKE</span>
<span class="absolute top-[22px] right-[18px] rotate-[14deg] rounded-lg border-[3px] border-stk02-no px-3 py-1.5 font-stk02-mono text-[15px] font-bold tracking-[0.08em] text-stk02-no opacity-0 transition-opacity duration-150 group-data-[hint=no]:opacity-100" aria-hidden="true">NOPE</span>
</article>
<article class="group absolute inset-0 rounded-[22px] overflow-hidden bg-stk02-card select-none cursor-grab active:cursor-grabbing [touch-action:none] shadow-[0_24px_50px_-24px_rgba(0,0,0,0.75)] transition-[transform,opacity] duration-500 ease-stk02 nth-last-2:scale-[0.94] nth-last-2:translate-y-4 nth-last-3:scale-[0.88] nth-last-3:translate-y-8 nth-last-[n+4]:scale-[0.82] nth-last-[n+4]:translate-y-[46px] nth-last-[n+4]:opacity-0 data-[dragging=true]:transition-none data-[gone]:opacity-0 data-[gone=yes]:[transform:translate(140%,-10%)_rotate(22deg)]! data-[gone=no]:[transform:translate(-140%,-10%)_rotate(-22deg)]! motion-reduce:transition-[opacity] motion-reduce:duration-200 focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-3" tabindex="0">
<img class="block w-full h-[74%] object-cover pointer-events-none" src="https://picsum.photos/seed/tw-swipe-4/400/460" width="400" height="460" alt="Forest path under tall pines">
<div class="px-[18px] py-4 text-white">
<h3 class="text-xl font-extrabold tracking-[-0.02em]">Rowan, 29</h3>
<p class="mt-0.5 text-[13px] text-stk02-muted">Botanist · Seattle</p>
</div>
<span class="absolute top-[22px] left-[18px] rotate-[-14deg] rounded-lg border-[3px] border-stk02-yes px-3 py-1.5 font-stk02-mono text-[15px] font-bold tracking-[0.08em] text-stk02-yes opacity-0 transition-opacity duration-150 group-data-[hint=yes]:opacity-100" aria-hidden="true">LIKE</span>
<span class="absolute top-[22px] right-[18px] rotate-[14deg] rounded-lg border-[3px] border-stk02-no px-3 py-1.5 font-stk02-mono text-[15px] font-bold tracking-[0.08em] text-stk02-no opacity-0 transition-opacity duration-150 group-data-[hint=no]:opacity-100" aria-hidden="true">NOPE</span>
</article>
</div>
<div class="flex gap-6">
<button class="grid place-items-center size-15 rounded-full border-0 bg-stk02-no text-2xl text-white cursor-pointer shadow-[0_10px_24px_-10px_rgba(0,0,0,0.6)] transition-transform duration-150 hover:-translate-y-[3px] hover:scale-105 active:scale-95 focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-3" id="stk-02-no" type="button" aria-label="Pass on this card">×</button>
<button class="grid place-items-center size-15 rounded-full border-0 bg-stk02-yes text-2xl text-white cursor-pointer shadow-[0_10px_24px_-10px_rgba(0,0,0,0.6)] transition-transform duration-150 hover:-translate-y-[3px] hover:scale-105 active:scale-95 focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-3" id="stk-02-yes" type="button" aria-label="Like this card">♥</button>
</div>
<p class="m-0 font-stk02-mono text-[11px] tracking-[0.14em] uppercase text-stk02-muted" id="stk-02-count" aria-live="polite"></p>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-stk02-bg: #15121f;
--color-stk02-bg: oklch(0.19 0.025 295);
--color-stk02-glow: #2a2340;
--color-stk02-glow: oklch(0.28 0.045 295);
--color-stk02-card: #241f36;
--color-stk02-card: oklch(0.26 0.04 295);
--color-stk02-muted: #c3bdd8;
--color-stk02-muted: oklch(0.81 0.03 295);
--color-stk02-yes: #45c98a;
--color-stk02-yes: oklch(0.70 0.17 150);
--color-stk02-no: #e2604a;
--color-stk02-no: oklch(0.65 0.20 20);
--font-stk02-sans: 'Archivo', system-ui, sans-serif;
--font-stk02-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-stk02: cubic-bezier(0.2, 0.9, 0.2, 1);
}
```
## JavaScript
```js
(() => {
const root = document.querySelector('.stk-02');
if (!root) return;
const deck = root.querySelector('#stk-02-deck');
const countEl = root.querySelector('#stk-02-count');
const THRESHOLD = 90;
const cards = () => [...deck.querySelectorAll('article')];
const top = () => cards()[cards().length - 1] || null;
function refresh() {
const t = top();
if (t) bind(t);
const n = cards().length;
countEl.textContent = n ? n + ' card' + (n > 1 ? 's' : '') + ' left' : 'Deck empty — reload to reshuffle';
}
/* State only: the exit transform is a data-[gone] utility in the markup, so the
destination stays visible in the class list rather than hidden in a script. */
function fling(card, dir) {
card.style.transform = '';
card.dataset.gone = dir > 0 ? 'yes' : 'no';
card.addEventListener('transitionend', () => { card.remove(); refresh(); }, { once: true });
}
function bind(card) {
if (card.dataset.bound) return;
card.dataset.bound = '1';
let startX = 0, dx = 0, dragging = false;
card.addEventListener('pointerdown', (e) => {
if (card !== top()) return;
dragging = true; startX = e.clientX;
card.setPointerCapture(e.pointerId);
card.dataset.dragging = 'true';
});
/* The only inline style in the demo, and it has to be: this value is the
pointer's live position and cannot exist as a static class. */
card.addEventListener('pointermove', (e) => {
if (!dragging) return;
dx = e.clientX - startX;
card.style.transform = 'translateX(' + dx + 'px) rotate(' + (dx / 18) + 'deg)';
if (dx > 30) card.dataset.hint = 'yes';
else if (dx < -30) card.dataset.hint = 'no';
else delete card.dataset.hint;
});
card.addEventListener('pointerup', () => {
if (!dragging) return;
dragging = false;
delete card.dataset.dragging;
if (Math.abs(dx) > THRESHOLD) { fling(card, dx > 0 ? 1 : -1); }
else { card.style.transform = ''; delete card.dataset.hint; }
dx = 0;
});
card.addEventListener('keydown', (e) => {
if (e.key === 'ArrowRight') fling(card, 1);
if (e.key === 'ArrowLeft') fling(card, -1);
});
}
root.querySelector('#stk-02-yes').addEventListener('click', () => { const t = top(); if (t) fling(t, 1); });
root.querySelector('#stk-02-no').addEventListener('click', () => { const t = top(); if (t) fling(t, -1); });
refresh();
})();
```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 Swipe Card Deck Drag to Dismiss
Source: https://codefronts.com/components/tailwind-stacked-cards/tailwind-swipe-card-deck-drag-to-dismiss/
The decision deck, ported to variants. The depth of the pile stops being three :nth-last-child rules and becomes nth-last-2:, nth-last-3: and nth-last-[n+4]: typed straight onto the card. The fly-off is a data-gone attribute the script sets, with the two exit transforms written as important arbitrary utilities so they beat the inline transform the drag leaves behind. The LIKE / NOPE stamps become real spans driven by group-data-[hint=yes]:, not content strings in a stylesheet.
## HTML
```html
<section class="stk-02 w-full min-h-svh flex flex-col items-center justify-center gap-[22px] px-5 py-10 font-stk02-sans bg-stk02-bg [background-image:radial-gradient(circle_at_50%_0%,var(--color-stk02-glow),transparent_70%)]" aria-label="Swipeable card deck">
<div class="relative w-[min(320px,86vw)] h-105" id="stk-02-deck">
<p class="absolute inset-0 grid place-items-center p-8 text-center text-base text-stk02-muted">That’s everyone for now.</p>
<article class="group absolute inset-0 rounded-[22px] overflow-hidden bg-stk02-card select-none cursor-grab active:cursor-grabbing [touch-action:none] shadow-[0_24px_50px_-24px_rgba(0,0,0,0.75)] transition-[transform,opacity] duration-500 ease-stk02 nth-last-2:scale-[0.94] nth-last-2:translate-y-4 nth-last-3:scale-[0.88] nth-last-3:translate-y-8 nth-last-[n+4]:scale-[0.82] nth-last-[n+4]:translate-y-[46px] nth-last-[n+4]:opacity-0 data-[dragging=true]:transition-none data-[gone]:opacity-0 data-[gone=yes]:[transform:translate(140%,-10%)_rotate(22deg)]! data-[gone=no]:[transform:translate(-140%,-10%)_rotate(-22deg)]! motion-reduce:transition-[opacity] motion-reduce:duration-200 focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-3" tabindex="0">
<img class="block w-full h-[74%] object-cover pointer-events-none" src="https://picsum.photos/seed/tw-swipe-1/400/460" width="400" height="460" alt="Mountain lake at first light">
<div class="px-[18px] py-4 text-white">
<h3 class="text-xl font-extrabold tracking-[-0.02em]">Aria, 27</h3>
<p class="mt-0.5 text-[13px] text-stk02-muted">Trail runner · Portland</p>
</div>
<span class="absolute top-[22px] left-[18px] rotate-[-14deg] rounded-lg border-[3px] border-stk02-yes px-3 py-1.5 font-stk02-mono text-[15px] font-bold tracking-[0.08em] text-stk02-yes opacity-0 transition-opacity duration-150 group-data-[hint=yes]:opacity-100" aria-hidden="true">LIKE</span>
<span class="absolute top-[22px] right-[18px] rotate-[14deg] rounded-lg border-[3px] border-stk02-no px-3 py-1.5 font-stk02-mono text-[15px] font-bold tracking-[0.08em] text-stk02-no opacity-0 transition-opacity duration-150 group-data-[hint=no]:opacity-100" aria-hidden="true">NOPE</span>
</article>
<article class="group absolute inset-0 rounded-[22px] overflow-hidden bg-stk02-card select-none cursor-grab active:cursor-grabbing [touch-action:none] shadow-[0_24px_50px_-24px_rgba(0,0,0,0.75)] transition-[transform,opacity] duration-500 ease-stk02 nth-last-2:scale-[0.94] nth-last-2:translate-y-4 nth-last-3:scale-[0.88] nth-last-3:translate-y-8 nth-last-[n+4]:scale-[0.82] nth-last-[n+4]:translate-y-[46px] nth-last-[n+4]:opacity-0 data-[dragging=true]:transition-none data-[gone]:opacity-0 data-[gone=yes]:[transform:translate(140%,-10%)_rotate(22deg)]! data-[gone=no]:[transform:translate(-140%,-10%)_rotate(-22deg)]! motion-reduce:transition-[opacity] motion-reduce:duration-200 focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-3" tabindex="0">
<img class="block w-full h-[74%] object-cover pointer-events-none" src="https://picsum.photos/seed/tw-swipe-2/400/460" width="400" height="460" alt="City rooftop in the evening">
<div class="px-[18px] py-4 text-white">
<h3 class="text-xl font-extrabold tracking-[-0.02em]">Devon, 31</h3>
<p class="mt-0.5 text-[13px] text-stk02-muted">Coffee nerd · Austin</p>
</div>
<span class="absolute top-[22px] left-[18px] rotate-[-14deg] rounded-lg border-[3px] border-stk02-yes px-3 py-1.5 font-stk02-mono text-[15px] font-bold tracking-[0.08em] text-stk02-yes opacity-0 transition-opacity duration-150 group-data-[hint=yes]:opacity-100" aria-hidden="true">LIKE</span>
<span class="absolute top-[22px] right-[18px] rotate-[14deg] rounded-lg border-[3px] border-stk02-no px-3 py-1.5 font-stk02-mono text-[15px] font-bold tracking-[0.08em] text-stk02-no opacity-0 transition-opacity duration-150 group-data-[hint=no]:opacity-100" aria-hidden="true">NOPE</span>
</article>
<article class="group absolute inset-0 rounded-[22px] overflow-hidden bg-stk02-card select-none cursor-grab active:cursor-grabbing [touch-action:none] shadow-[0_24px_50px_-24px_rgba(0,0,0,0.75)] transition-[transform,opacity] duration-500 ease-stk02 nth-last-2:scale-[0.94] nth-last-2:translate-y-4 nth-last-3:scale-[0.88] nth-last-3:translate-y-8 nth-last-[n+4]:scale-[0.82] nth-last-[n+4]:translate-y-[46px] nth-last-[n+4]:opacity-0 data-[dragging=true]:transition-none data-[gone]:opacity-0 data-[gone=yes]:[transform:translate(140%,-10%)_rotate(22deg)]! data-[gone=no]:[transform:translate(-140%,-10%)_rotate(-22deg)]! motion-reduce:transition-[opacity] motion-reduce:duration-200 focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-3" tabindex="0">
<img class="block w-full h-[74%] object-cover pointer-events-none" src="https://picsum.photos/seed/tw-swipe-3/400/460" width="400" height="460" alt="Empty desert road">
<div class="px-[18px] py-4 text-white">
<h3 class="text-xl font-extrabold tracking-[-0.02em]">Sky, 24</h3>
<p class="mt-0.5 text-[13px] text-stk02-muted">Van-lifer · Denver</p>
</div>
<span class="absolute top-[22px] left-[18px] rotate-[-14deg] rounded-lg border-[3px] border-stk02-yes px-3 py-1.5 font-stk02-mono text-[15px] font-bold tracking-[0.08em] text-stk02-yes opacity-0 transition-opacity duration-150 group-data-[hint=yes]:opacity-100" aria-hidden="true">LIKE</span>
<span class="absolute top-[22px] right-[18px] rotate-[14deg] rounded-lg border-[3px] border-stk02-no px-3 py-1.5 font-stk02-mono text-[15px] font-bold tracking-[0.08em] text-stk02-no opacity-0 transition-opacity duration-150 group-data-[hint=no]:opacity-100" aria-hidden="true">NOPE</span>
</article>
<article class="group absolute inset-0 rounded-[22px] overflow-hidden bg-stk02-card select-none cursor-grab active:cursor-grabbing [touch-action:none] shadow-[0_24px_50px_-24px_rgba(0,0,0,0.75)] transition-[transform,opacity] duration-500 ease-stk02 nth-last-2:scale-[0.94] nth-last-2:translate-y-4 nth-last-3:scale-[0.88] nth-last-3:translate-y-8 nth-last-[n+4]:scale-[0.82] nth-last-[n+4]:translate-y-[46px] nth-last-[n+4]:opacity-0 data-[dragging=true]:transition-none data-[gone]:opacity-0 data-[gone=yes]:[transform:translate(140%,-10%)_rotate(22deg)]! data-[gone=no]:[transform:translate(-140%,-10%)_rotate(-22deg)]! motion-reduce:transition-[opacity] motion-reduce:duration-200 focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-3" tabindex="0">
<img class="block w-full h-[74%] object-cover pointer-events-none" src="https://picsum.photos/seed/tw-swipe-4/400/460" width="400" height="460" alt="Forest path under tall pines">
<div class="px-[18px] py-4 text-white">
<h3 class="text-xl font-extrabold tracking-[-0.02em]">Rowan, 29</h3>
<p class="mt-0.5 text-[13px] text-stk02-muted">Botanist · Seattle</p>
</div>
<span class="absolute top-[22px] left-[18px] rotate-[-14deg] rounded-lg border-[3px] border-stk02-yes px-3 py-1.5 font-stk02-mono text-[15px] font-bold tracking-[0.08em] text-stk02-yes opacity-0 transition-opacity duration-150 group-data-[hint=yes]:opacity-100" aria-hidden="true">LIKE</span>
<span class="absolute top-[22px] right-[18px] rotate-[14deg] rounded-lg border-[3px] border-stk02-no px-3 py-1.5 font-stk02-mono text-[15px] font-bold tracking-[0.08em] text-stk02-no opacity-0 transition-opacity duration-150 group-data-[hint=no]:opacity-100" aria-hidden="true">NOPE</span>
</article>
</div>
<div class="flex gap-6">
<button class="grid place-items-center size-15 rounded-full border-0 bg-stk02-no text-2xl text-white cursor-pointer shadow-[0_10px_24px_-10px_rgba(0,0,0,0.6)] transition-transform duration-150 hover:-translate-y-[3px] hover:scale-105 active:scale-95 focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-3" id="stk-02-no" type="button" aria-label="Pass on this card">×</button>
<button class="grid place-items-center size-15 rounded-full border-0 bg-stk02-yes text-2xl text-white cursor-pointer shadow-[0_10px_24px_-10px_rgba(0,0,0,0.6)] transition-transform duration-150 hover:-translate-y-[3px] hover:scale-105 active:scale-95 focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-3" id="stk-02-yes" type="button" aria-label="Like this card">♥</button>
</div>
<p class="m-0 font-stk02-mono text-[11px] tracking-[0.14em] uppercase text-stk02-muted" id="stk-02-count" aria-live="polite"></p>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-stk02-bg: #15121f;
--color-stk02-bg: oklch(0.19 0.025 295);
--color-stk02-glow: #2a2340;
--color-stk02-glow: oklch(0.28 0.045 295);
--color-stk02-card: #241f36;
--color-stk02-card: oklch(0.26 0.04 295);
--color-stk02-muted: #c3bdd8;
--color-stk02-muted: oklch(0.81 0.03 295);
--color-stk02-yes: #45c98a;
--color-stk02-yes: oklch(0.70 0.17 150);
--color-stk02-no: #e2604a;
--color-stk02-no: oklch(0.65 0.20 20);
--font-stk02-sans: 'Archivo', system-ui, sans-serif;
--font-stk02-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-stk02: cubic-bezier(0.2, 0.9, 0.2, 1);
}
```
## JavaScript
```js
(() => {
const root = document.querySelector('.stk-02');
if (!root) return;
const deck = root.querySelector('#stk-02-deck');
const countEl = root.querySelector('#stk-02-count');
const THRESHOLD = 90;
const cards = () => [...deck.querySelectorAll('article')];
const top = () => cards()[cards().length - 1] || null;
function refresh() {
const t = top();
if (t) bind(t);
const n = cards().length;
countEl.textContent = n ? n + ' card' + (n > 1 ? 's' : '') + ' left' : 'Deck empty — reload to reshuffle';
}
/* State only: the exit transform is a data-[gone] utility in the markup, so the
destination stays visible in the class list rather than hidden in a script. */
function fling(card, dir) {
card.style.transform = '';
card.dataset.gone = dir > 0 ? 'yes' : 'no';
card.addEventListener('transitionend', () => { card.remove(); refresh(); }, { once: true });
}
function bind(card) {
if (card.dataset.bound) return;
card.dataset.bound = '1';
let startX = 0, dx = 0, dragging = false;
card.addEventListener('pointerdown', (e) => {
if (card !== top()) return;
dragging = true; startX = e.clientX;
card.setPointerCapture(e.pointerId);
card.dataset.dragging = 'true';
});
/* The only inline style in the demo, and it has to be: this value is the
pointer's live position and cannot exist as a static class. */
card.addEventListener('pointermove', (e) => {
if (!dragging) return;
dx = e.clientX - startX;
card.style.transform = 'translateX(' + dx + 'px) rotate(' + (dx / 18) + 'deg)';
if (dx > 30) card.dataset.hint = 'yes';
else if (dx < -30) card.dataset.hint = 'no';
else delete card.dataset.hint;
});
card.addEventListener('pointerup', () => {
if (!dragging) return;
dragging = false;
delete card.dataset.dragging;
if (Math.abs(dx) > THRESHOLD) { fling(card, dx > 0 ? 1 : -1); }
else { card.style.transform = ''; delete card.dataset.hint; }
dx = 0;
});
card.addEventListener('keydown', (e) => {
if (e.key === 'ArrowRight') fling(card, 1);
if (e.key === 'ArrowLeft') fling(card, -1);
});
}
root.querySelector('#stk-02-yes').addEventListener('click', () => { const t = top(); if (t) fling(t, 1); });
root.querySelector('#stk-02-no').addEventListener('click', () => { const t = top(); if (t) fling(t, -1); });
refresh();
})();
```How this works
Pile depth is a structural variant, and this is the port's clearest win. The CSS version writes three :nth-last-child blocks that live far from the card they describe. In v4 the card says it itself:
class="… nth-last-2:scale-[0.94] nth-last-2:translate-y-4
nth-last-3:scale-[0.88] nth-last-3:translate-y-8
nth-last-[n+4]:scale-[0.82] nth-last-[n+4]:translate-y-[46px]
nth-last-[n+4]:opacity-0"Because the selector counts from the end, removing the top card automatically promotes everything beneath it — no index bookkeeping in the script at all.
The script writes state; the stylesheet writes the exit. During a drag the handler must set an inline transform: the value is the pointer's live position and cannot be a class. But the fling is a fixed destination, so it goes back into utility-land as data-gone:
data-[gone=yes]:[transform:translate(140%,-10%)_rotate(22deg)]!
data-[gone=no]:[transform:translate(-140%,-10%)_rotate(-22deg)]!
data-[gone]:opacity-0The ! is load-bearing. An inline style normally beats a stylesheet, so without important the card would freeze at wherever the finger let go.
Stamps as elements, not pseudo-content. The CSS version paints LIKE and NOPE with ::before/::after and content. Tailwind can express that (before:content-['LIKE']) but should not here: real spans are inspectable, translatable, and can be marked aria-hidden so the stamp never doubles the announcement of a card the screen reader has already read.
Keyboard parity is not optional. Each card is focusable and answers Arrow Left / Arrow Right with the same fling the buttons trigger, and the remaining count sits in an aria-live="polite" region.
Make it yours
- Change the swipe commitment distance via the
THRESHOLDconstant — 90px is a deliberate, forgiving default on touch. - Restyle the fly-off arc by editing the two
data-[gone=…]arbitrary transforms; the rotation multiplier for the live drag lives in the script. - Deepen the pile by adding a
nth-last-4:step and pushing thenth-last-[n+4]:cutoff out to[n+5]. - Recolour the stamps and buttons through
--color-stk02-yes/--color-stk02-noin@theme— both the stamp text, its border and the button fill read the same token. - Swap the images for your own and keep the
width/heightattributes; they are what hold CLS at zero.
Gotchas — read before shipping
- The
!important modifier on the exit transforms is required, not stylistic. Inline styles outrank stylesheet rules, and the drag leaves an inlinetransformbehind. - Bind pointer listeners to the current top card only, or drags leak to buried cards. The script rebinds on every removal and guards with a
data-boundflag. [touch-action:none]has no utility and is mandatory. Without it the browser claims the gesture and the page scrolls instead of the card moving.- Use
pointerevents rather than separate touch and mouse paths, and callsetPointerCaptureso a fast drag that leaves the card still delivers itspointerup. nth-last-[n+4]:needs the bracket form — only small fixed positions have bare utilities (nth-last-3:).- Keep the button row. A deck that can only be driven by a drag gesture is unusable by keyboard and by anyone with a motor impairment.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
Pointer Events and CSS transforms are universal (Chrome 55+, Safari 13+, Firefox 59+); the floor here is Tailwind v4's own generated sheet. :nth-last-child() is CSS3 and needs nothing modern.