09 / 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 Click to Front Stacked Cards Cycle
A queue you browse by clicking the front card: it flings sideways, drops to the bottom of the pile and glides back in at the rear. Depth is a number, not DOM order — every card reads its own --depth through [transform:translateY(calc(var(--depth)*16px))_scale(calc(1-var(--depth)*0.05))] and [z-index:calc(10-var(--depth))], so the script only ever repaints one integer per card.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="stk-09 w-full min-h-svh flex flex-col items-center justify-center gap-7 px-5 py-12 font-stk09-sans bg-stk09-bg" aria-label="Click-to-front card deck">
<div class="relative w-[min(320px,86vw)] h-55" id="stk-09-deck">
<button class="absolute inset-0 flex flex-col gap-2 border-0 cursor-pointer rounded-[20px] bg-stk09-c1 p-6 text-left text-white shadow-[0_24px_46px_-26px_rgba(0,0,0,0.8)] [transform:translateY(calc(var(--depth,0)*16px))_scale(calc(1-var(--depth,0)*0.05))] [z-index:calc(10-var(--depth,0))] transition-[transform,opacity] duration-500 ease-stk09 data-[sending=true]:opacity-0 data-[sending=true]:duration-450 data-[sending=true]:ease-in data-[sending=true]:[transform:translateX(130%)_rotate(12deg)] focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-3 motion-reduce:duration-200" type="button">
<span class="font-stk09-mono text-xs font-bold tracking-[0.08em] opacity-85">Ticket #4821</span>
<b class="text-xl font-extrabold tracking-[-0.02em]">Payment retry loop</b>
<i class="mt-auto font-stk09-mono text-xs not-italic opacity-85">High · Billing</i>
</button>
<button class="absolute inset-0 flex flex-col gap-2 border-0 cursor-pointer rounded-[20px] bg-stk09-c2 p-6 text-left text-white shadow-[0_24px_46px_-26px_rgba(0,0,0,0.8)] [transform:translateY(calc(var(--depth,0)*16px))_scale(calc(1-var(--depth,0)*0.05))] [z-index:calc(10-var(--depth,0))] transition-[transform,opacity] duration-500 ease-stk09 data-[sending=true]:opacity-0 data-[sending=true]:duration-450 data-[sending=true]:ease-in data-[sending=true]:[transform:translateX(130%)_rotate(12deg)] focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-3 motion-reduce:duration-200" type="button">
<span class="font-stk09-mono text-xs font-bold tracking-[0.08em] opacity-85">Ticket #4822</span>
<b class="text-xl font-extrabold tracking-[-0.02em]">Slow search on mobile</b>
<i class="mt-auto font-stk09-mono text-xs not-italic opacity-85">Medium · Web</i>
</button>
<button class="absolute inset-0 flex flex-col gap-2 border-0 cursor-pointer rounded-[20px] bg-stk09-c3 p-6 text-left text-white shadow-[0_24px_46px_-26px_rgba(0,0,0,0.8)] [transform:translateY(calc(var(--depth,0)*16px))_scale(calc(1-var(--depth,0)*0.05))] [z-index:calc(10-var(--depth,0))] transition-[transform,opacity] duration-500 ease-stk09 data-[sending=true]:opacity-0 data-[sending=true]:duration-450 data-[sending=true]:ease-in data-[sending=true]:[transform:translateX(130%)_rotate(12deg)] focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-3 motion-reduce:duration-200" type="button">
<span class="font-stk09-mono text-xs font-bold tracking-[0.08em] opacity-85">Ticket #4823</span>
<b class="text-xl font-extrabold tracking-[-0.02em]">Export to CSV missing</b>
<i class="mt-auto font-stk09-mono text-xs not-italic opacity-85">Low · Reports</i>
</button>
<button class="absolute inset-0 flex flex-col gap-2 border-0 cursor-pointer rounded-[20px] bg-stk09-c4 p-6 text-left text-white shadow-[0_24px_46px_-26px_rgba(0,0,0,0.8)] [transform:translateY(calc(var(--depth,0)*16px))_scale(calc(1-var(--depth,0)*0.05))] [z-index:calc(10-var(--depth,0))] transition-[transform,opacity] duration-500 ease-stk09 data-[sending=true]:opacity-0 data-[sending=true]:duration-450 data-[sending=true]:ease-in data-[sending=true]:[transform:translateX(130%)_rotate(12deg)] focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-3 motion-reduce:duration-200" type="button">
<span class="font-stk09-mono text-xs font-bold tracking-[0.08em] opacity-85">Ticket #4824</span>
<b class="text-xl font-extrabold tracking-[-0.02em]">SSO redirect fails</b>
<i class="mt-auto font-stk09-mono text-xs not-italic opacity-85">High · Auth</i>
</button>
</div>
<div class="flex flex-col items-center gap-2">
<button class="rounded-xl border border-stk09-line bg-stk09-btn px-5.5 py-2.75 text-sm font-bold text-white cursor-pointer transition-colors duration-200 hover:bg-stk09-btnh focus-visible:outline-3 focus-visible:outline-stk09-ring focus-visible:outline-offset-2" id="stk-09-next" type="button">Next ticket ↻</button>
<p class="sr-only" id="stk-09-live" aria-live="polite"></p>
</div>
</section><section class="stk-09 w-full min-h-svh flex flex-col items-center justify-center gap-7 px-5 py-12 font-stk09-sans bg-stk09-bg" aria-label="Click-to-front card deck">
<div class="relative w-[min(320px,86vw)] h-55" id="stk-09-deck">
<button class="absolute inset-0 flex flex-col gap-2 border-0 cursor-pointer rounded-[20px] bg-stk09-c1 p-6 text-left text-white shadow-[0_24px_46px_-26px_rgba(0,0,0,0.8)] [transform:translateY(calc(var(--depth,0)*16px))_scale(calc(1-var(--depth,0)*0.05))] [z-index:calc(10-var(--depth,0))] transition-[transform,opacity] duration-500 ease-stk09 data-[sending=true]:opacity-0 data-[sending=true]:duration-450 data-[sending=true]:ease-in data-[sending=true]:[transform:translateX(130%)_rotate(12deg)] focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-3 motion-reduce:duration-200" type="button">
<span class="font-stk09-mono text-xs font-bold tracking-[0.08em] opacity-85">Ticket #4821</span>
<b class="text-xl font-extrabold tracking-[-0.02em]">Payment retry loop</b>
<i class="mt-auto font-stk09-mono text-xs not-italic opacity-85">High · Billing</i>
</button>
<button class="absolute inset-0 flex flex-col gap-2 border-0 cursor-pointer rounded-[20px] bg-stk09-c2 p-6 text-left text-white shadow-[0_24px_46px_-26px_rgba(0,0,0,0.8)] [transform:translateY(calc(var(--depth,0)*16px))_scale(calc(1-var(--depth,0)*0.05))] [z-index:calc(10-var(--depth,0))] transition-[transform,opacity] duration-500 ease-stk09 data-[sending=true]:opacity-0 data-[sending=true]:duration-450 data-[sending=true]:ease-in data-[sending=true]:[transform:translateX(130%)_rotate(12deg)] focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-3 motion-reduce:duration-200" type="button">
<span class="font-stk09-mono text-xs font-bold tracking-[0.08em] opacity-85">Ticket #4822</span>
<b class="text-xl font-extrabold tracking-[-0.02em]">Slow search on mobile</b>
<i class="mt-auto font-stk09-mono text-xs not-italic opacity-85">Medium · Web</i>
</button>
<button class="absolute inset-0 flex flex-col gap-2 border-0 cursor-pointer rounded-[20px] bg-stk09-c3 p-6 text-left text-white shadow-[0_24px_46px_-26px_rgba(0,0,0,0.8)] [transform:translateY(calc(var(--depth,0)*16px))_scale(calc(1-var(--depth,0)*0.05))] [z-index:calc(10-var(--depth,0))] transition-[transform,opacity] duration-500 ease-stk09 data-[sending=true]:opacity-0 data-[sending=true]:duration-450 data-[sending=true]:ease-in data-[sending=true]:[transform:translateX(130%)_rotate(12deg)] focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-3 motion-reduce:duration-200" type="button">
<span class="font-stk09-mono text-xs font-bold tracking-[0.08em] opacity-85">Ticket #4823</span>
<b class="text-xl font-extrabold tracking-[-0.02em]">Export to CSV missing</b>
<i class="mt-auto font-stk09-mono text-xs not-italic opacity-85">Low · Reports</i>
</button>
<button class="absolute inset-0 flex flex-col gap-2 border-0 cursor-pointer rounded-[20px] bg-stk09-c4 p-6 text-left text-white shadow-[0_24px_46px_-26px_rgba(0,0,0,0.8)] [transform:translateY(calc(var(--depth,0)*16px))_scale(calc(1-var(--depth,0)*0.05))] [z-index:calc(10-var(--depth,0))] transition-[transform,opacity] duration-500 ease-stk09 data-[sending=true]:opacity-0 data-[sending=true]:duration-450 data-[sending=true]:ease-in data-[sending=true]:[transform:translateX(130%)_rotate(12deg)] focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-3 motion-reduce:duration-200" type="button">
<span class="font-stk09-mono text-xs font-bold tracking-[0.08em] opacity-85">Ticket #4824</span>
<b class="text-xl font-extrabold tracking-[-0.02em]">SSO redirect fails</b>
<i class="mt-auto font-stk09-mono text-xs not-italic opacity-85">High · Auth</i>
</button>
</div>
<div class="flex flex-col items-center gap-2">
<button class="rounded-xl border border-stk09-line bg-stk09-btn px-5.5 py-2.75 text-sm font-bold text-white cursor-pointer transition-colors duration-200 hover:bg-stk09-btnh focus-visible:outline-3 focus-visible:outline-stk09-ring focus-visible:outline-offset-2" id="stk-09-next" type="button">Next ticket ↻</button>
<p class="sr-only" id="stk-09-live" aria-live="polite"></p>
</div>
</section>@import "tailwindcss";
@theme {
--color-stk09-bg: #101219;
--color-stk09-bg: oklch(0.19 0.012 265);
--color-stk09-btn: #232838;
--color-stk09-btn: oklch(0.28 0.018 265);
--color-stk09-btnh: #2e3446;
--color-stk09-btnh: oklch(0.33 0.02 265);
--color-stk09-line: #333a4e;
--color-stk09-line: oklch(0.36 0.02 265);
--color-stk09-ring: #5aa2e0;
--color-stk09-ring: oklch(0.66 0.15 250);
--color-stk09-c1: #d2604c;
--color-stk09-c1: oklch(0.63 0.17 20);
--color-stk09-c2: #45ab7c;
--color-stk09-c2: oklch(0.66 0.15 150);
--color-stk09-c3: #4d8ade;
--color-stk09-c3: oklch(0.64 0.16 250);
--color-stk09-c4: #9b6ed6;
--color-stk09-c4: oklch(0.64 0.16 300);
--font-stk09-sans: 'Archivo', system-ui, sans-serif;
--font-stk09-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-stk09: cubic-bezier(0.3, 0.9, 0.3, 1);
}@import "tailwindcss";
@theme {
--color-stk09-bg: #101219;
--color-stk09-bg: oklch(0.19 0.012 265);
--color-stk09-btn: #232838;
--color-stk09-btn: oklch(0.28 0.018 265);
--color-stk09-btnh: #2e3446;
--color-stk09-btnh: oklch(0.33 0.02 265);
--color-stk09-line: #333a4e;
--color-stk09-line: oklch(0.36 0.02 265);
--color-stk09-ring: #5aa2e0;
--color-stk09-ring: oklch(0.66 0.15 250);
--color-stk09-c1: #d2604c;
--color-stk09-c1: oklch(0.63 0.17 20);
--color-stk09-c2: #45ab7c;
--color-stk09-c2: oklch(0.66 0.15 150);
--color-stk09-c3: #4d8ade;
--color-stk09-c3: oklch(0.64 0.16 250);
--color-stk09-c4: #9b6ed6;
--color-stk09-c4: oklch(0.64 0.16 300);
--font-stk09-sans: 'Archivo', system-ui, sans-serif;
--font-stk09-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-stk09: cubic-bezier(0.3, 0.9, 0.3, 1);
}(() => {
const root = document.querySelector('.stk-09');
if (!root) return;
const deck = root.querySelector('#stk-09-deck');
const live = root.querySelector('#stk-09-live');
/* The controller writes ONE integer per card. Translate, scale and z-index
are all derived from it by arbitrary calc() utilities in the markup. */
const paint = () => [...deck.children].forEach((c, i) => c.style.setProperty('--depth', i));
function cycle() {
const front = deck.firstElementChild;
if (!front) return;
front.dataset.sending = 'true';
front.addEventListener('transitionend', () => {
delete front.dataset.sending;
deck.appendChild(front);
paint();
const title = deck.firstElementChild.querySelector('b');
if (title) live.textContent = 'Now showing ' + title.textContent;
}, { once: true });
}
deck.addEventListener('click', (e) => {
if (e.target.closest('button') === deck.firstElementChild) cycle();
});
root.querySelector('#stk-09-next').addEventListener('click', cycle);
paint();
})();(() => {
const root = document.querySelector('.stk-09');
if (!root) return;
const deck = root.querySelector('#stk-09-deck');
const live = root.querySelector('#stk-09-live');
/* The controller writes ONE integer per card. Translate, scale and z-index
are all derived from it by arbitrary calc() utilities in the markup. */
const paint = () => [...deck.children].forEach((c, i) => c.style.setProperty('--depth', i));
function cycle() {
const front = deck.firstElementChild;
if (!front) return;
front.dataset.sending = 'true';
front.addEventListener('transitionend', () => {
delete front.dataset.sending;
deck.appendChild(front);
paint();
const title = deck.firstElementChild.querySelector('b');
if (title) live.textContent = 'Now showing ' + title.textContent;
}, { once: true });
}
deck.addEventListener('click', (e) => {
if (e.target.closest('button') === deck.firstElementChild) cycle();
});
root.querySelector('#stk-09-next').addEventListener('click', cycle);
paint();
})();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-stk09-bg: #101219;
--color-stk09-bg: oklch(0.19 0.012 265);
--color-stk09-btn: #232838;
--color-stk09-btn: oklch(0.28 0.018 265);
--color-stk09-btnh: #2e3446;
--color-stk09-btnh: oklch(0.33 0.02 265);
--color-stk09-line: #333a4e;
--color-stk09-line: oklch(0.36 0.02 265);
--color-stk09-ring: #5aa2e0;
--color-stk09-ring: oklch(0.66 0.15 250);
--color-stk09-c1: #d2604c;
--color-stk09-c1: oklch(0.63 0.17 20);
--color-stk09-c2: #45ab7c;
--color-stk09-c2: oklch(0.66 0.15 150);
--color-stk09-c3: #4d8ade;
--color-stk09-c3: oklch(0.64 0.16 250);
--color-stk09-c4: #9b6ed6;
--color-stk09-c4: oklch(0.64 0.16 300);
--font-stk09-sans: 'Archivo', system-ui, sans-serif;
--font-stk09-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-stk09: cubic-bezier(0.3, 0.9, 0.3, 1);
}@import "tailwindcss";
@theme {
--color-stk09-bg: #101219;
--color-stk09-bg: oklch(0.19 0.012 265);
--color-stk09-btn: #232838;
--color-stk09-btn: oklch(0.28 0.018 265);
--color-stk09-btnh: #2e3446;
--color-stk09-btnh: oklch(0.33 0.02 265);
--color-stk09-line: #333a4e;
--color-stk09-line: oklch(0.36 0.02 265);
--color-stk09-ring: #5aa2e0;
--color-stk09-ring: oklch(0.66 0.15 250);
--color-stk09-c1: #d2604c;
--color-stk09-c1: oklch(0.63 0.17 20);
--color-stk09-c2: #45ab7c;
--color-stk09-c2: oklch(0.66 0.15 150);
--color-stk09-c3: #4d8ade;
--color-stk09-c3: oklch(0.64 0.16 250);
--color-stk09-c4: #9b6ed6;
--color-stk09-c4: oklch(0.64 0.16 300);
--font-stk09-sans: 'Archivo', system-ui, sans-serif;
--font-stk09-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-stk09: cubic-bezier(0.3, 0.9, 0.3, 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,700;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,700;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. - One number does three jobs:
--depthfeeds the translate, the scale and[z-index:calc(10-var(--depth,0))], so a reshuffle is a single integer write per card. z-indexhas no calc-able utility and never will — this is exactly the case arbitrary properties are for.- The
.sendingclass becomesdata-[sending=true]:, which also carries its ownduration-450 ease-in: the exit is faster and sharper than the settle, described on the element rather than in a second rule. - The
--depthfallback is written into the utility (var(--depth,0)), so the deck renders correctly before the script runs — no flash of stacked-at-zero cards. - Cards are real
<button>s, which removes thetabindexand the syntheticEnter/Spacehandling the CSS version needs. - The scoped sr-only helper becomes Tailwind's
sr-onlyon thearia-liveregion — the one part of this demo a sighted user never sees and a screen-reader user depends on.
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 Click to Front Stacked Cards Cycle
Source: https://codefronts.com/components/tailwind-stacked-cards/tailwind-click-to-front-stacked-cards-cycle/
A queue you browse by clicking the front card: it flings sideways, drops to the bottom of the pile and glides back in at the rear. Depth is a number, not DOM order — every card reads its own --depth through [transform:translateY(calc(var(--depth)*16px))_scale(calc(1-var(--depth)*0.05))] and [z-index:calc(10-var(--depth))], so the script only ever repaints one integer per card.
## HTML
```html
<section class="stk-09 w-full min-h-svh flex flex-col items-center justify-center gap-7 px-5 py-12 font-stk09-sans bg-stk09-bg" aria-label="Click-to-front card deck">
<div class="relative w-[min(320px,86vw)] h-55" id="stk-09-deck">
<button class="absolute inset-0 flex flex-col gap-2 border-0 cursor-pointer rounded-[20px] bg-stk09-c1 p-6 text-left text-white shadow-[0_24px_46px_-26px_rgba(0,0,0,0.8)] [transform:translateY(calc(var(--depth,0)*16px))_scale(calc(1-var(--depth,0)*0.05))] [z-index:calc(10-var(--depth,0))] transition-[transform,opacity] duration-500 ease-stk09 data-[sending=true]:opacity-0 data-[sending=true]:duration-450 data-[sending=true]:ease-in data-[sending=true]:[transform:translateX(130%)_rotate(12deg)] focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-3 motion-reduce:duration-200" type="button">
<span class="font-stk09-mono text-xs font-bold tracking-[0.08em] opacity-85">Ticket #4821</span>
<b class="text-xl font-extrabold tracking-[-0.02em]">Payment retry loop</b>
<i class="mt-auto font-stk09-mono text-xs not-italic opacity-85">High · Billing</i>
</button>
<button class="absolute inset-0 flex flex-col gap-2 border-0 cursor-pointer rounded-[20px] bg-stk09-c2 p-6 text-left text-white shadow-[0_24px_46px_-26px_rgba(0,0,0,0.8)] [transform:translateY(calc(var(--depth,0)*16px))_scale(calc(1-var(--depth,0)*0.05))] [z-index:calc(10-var(--depth,0))] transition-[transform,opacity] duration-500 ease-stk09 data-[sending=true]:opacity-0 data-[sending=true]:duration-450 data-[sending=true]:ease-in data-[sending=true]:[transform:translateX(130%)_rotate(12deg)] focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-3 motion-reduce:duration-200" type="button">
<span class="font-stk09-mono text-xs font-bold tracking-[0.08em] opacity-85">Ticket #4822</span>
<b class="text-xl font-extrabold tracking-[-0.02em]">Slow search on mobile</b>
<i class="mt-auto font-stk09-mono text-xs not-italic opacity-85">Medium · Web</i>
</button>
<button class="absolute inset-0 flex flex-col gap-2 border-0 cursor-pointer rounded-[20px] bg-stk09-c3 p-6 text-left text-white shadow-[0_24px_46px_-26px_rgba(0,0,0,0.8)] [transform:translateY(calc(var(--depth,0)*16px))_scale(calc(1-var(--depth,0)*0.05))] [z-index:calc(10-var(--depth,0))] transition-[transform,opacity] duration-500 ease-stk09 data-[sending=true]:opacity-0 data-[sending=true]:duration-450 data-[sending=true]:ease-in data-[sending=true]:[transform:translateX(130%)_rotate(12deg)] focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-3 motion-reduce:duration-200" type="button">
<span class="font-stk09-mono text-xs font-bold tracking-[0.08em] opacity-85">Ticket #4823</span>
<b class="text-xl font-extrabold tracking-[-0.02em]">Export to CSV missing</b>
<i class="mt-auto font-stk09-mono text-xs not-italic opacity-85">Low · Reports</i>
</button>
<button class="absolute inset-0 flex flex-col gap-2 border-0 cursor-pointer rounded-[20px] bg-stk09-c4 p-6 text-left text-white shadow-[0_24px_46px_-26px_rgba(0,0,0,0.8)] [transform:translateY(calc(var(--depth,0)*16px))_scale(calc(1-var(--depth,0)*0.05))] [z-index:calc(10-var(--depth,0))] transition-[transform,opacity] duration-500 ease-stk09 data-[sending=true]:opacity-0 data-[sending=true]:duration-450 data-[sending=true]:ease-in data-[sending=true]:[transform:translateX(130%)_rotate(12deg)] focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-3 motion-reduce:duration-200" type="button">
<span class="font-stk09-mono text-xs font-bold tracking-[0.08em] opacity-85">Ticket #4824</span>
<b class="text-xl font-extrabold tracking-[-0.02em]">SSO redirect fails</b>
<i class="mt-auto font-stk09-mono text-xs not-italic opacity-85">High · Auth</i>
</button>
</div>
<div class="flex flex-col items-center gap-2">
<button class="rounded-xl border border-stk09-line bg-stk09-btn px-5.5 py-2.75 text-sm font-bold text-white cursor-pointer transition-colors duration-200 hover:bg-stk09-btnh focus-visible:outline-3 focus-visible:outline-stk09-ring focus-visible:outline-offset-2" id="stk-09-next" type="button">Next ticket ↻</button>
<p class="sr-only" id="stk-09-live" aria-live="polite"></p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-stk09-bg: #101219;
--color-stk09-bg: oklch(0.19 0.012 265);
--color-stk09-btn: #232838;
--color-stk09-btn: oklch(0.28 0.018 265);
--color-stk09-btnh: #2e3446;
--color-stk09-btnh: oklch(0.33 0.02 265);
--color-stk09-line: #333a4e;
--color-stk09-line: oklch(0.36 0.02 265);
--color-stk09-ring: #5aa2e0;
--color-stk09-ring: oklch(0.66 0.15 250);
--color-stk09-c1: #d2604c;
--color-stk09-c1: oklch(0.63 0.17 20);
--color-stk09-c2: #45ab7c;
--color-stk09-c2: oklch(0.66 0.15 150);
--color-stk09-c3: #4d8ade;
--color-stk09-c3: oklch(0.64 0.16 250);
--color-stk09-c4: #9b6ed6;
--color-stk09-c4: oklch(0.64 0.16 300);
--font-stk09-sans: 'Archivo', system-ui, sans-serif;
--font-stk09-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-stk09: cubic-bezier(0.3, 0.9, 0.3, 1);
}
```
## JavaScript
```js
(() => {
const root = document.querySelector('.stk-09');
if (!root) return;
const deck = root.querySelector('#stk-09-deck');
const live = root.querySelector('#stk-09-live');
/* The controller writes ONE integer per card. Translate, scale and z-index
are all derived from it by arbitrary calc() utilities in the markup. */
const paint = () => [...deck.children].forEach((c, i) => c.style.setProperty('--depth', i));
function cycle() {
const front = deck.firstElementChild;
if (!front) return;
front.dataset.sending = 'true';
front.addEventListener('transitionend', () => {
delete front.dataset.sending;
deck.appendChild(front);
paint();
const title = deck.firstElementChild.querySelector('b');
if (title) live.textContent = 'Now showing ' + title.textContent;
}, { once: true });
}
deck.addEventListener('click', (e) => {
if (e.target.closest('button') === deck.firstElementChild) cycle();
});
root.querySelector('#stk-09-next').addEventListener('click', cycle);
paint();
})();
```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 Click to Front Stacked Cards Cycle
Source: https://codefronts.com/components/tailwind-stacked-cards/tailwind-click-to-front-stacked-cards-cycle/
A queue you browse by clicking the front card: it flings sideways, drops to the bottom of the pile and glides back in at the rear. Depth is a number, not DOM order — every card reads its own --depth through [transform:translateY(calc(var(--depth)*16px))_scale(calc(1-var(--depth)*0.05))] and [z-index:calc(10-var(--depth))], so the script only ever repaints one integer per card.
## HTML
```html
<section class="stk-09 w-full min-h-svh flex flex-col items-center justify-center gap-7 px-5 py-12 font-stk09-sans bg-stk09-bg" aria-label="Click-to-front card deck">
<div class="relative w-[min(320px,86vw)] h-55" id="stk-09-deck">
<button class="absolute inset-0 flex flex-col gap-2 border-0 cursor-pointer rounded-[20px] bg-stk09-c1 p-6 text-left text-white shadow-[0_24px_46px_-26px_rgba(0,0,0,0.8)] [transform:translateY(calc(var(--depth,0)*16px))_scale(calc(1-var(--depth,0)*0.05))] [z-index:calc(10-var(--depth,0))] transition-[transform,opacity] duration-500 ease-stk09 data-[sending=true]:opacity-0 data-[sending=true]:duration-450 data-[sending=true]:ease-in data-[sending=true]:[transform:translateX(130%)_rotate(12deg)] focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-3 motion-reduce:duration-200" type="button">
<span class="font-stk09-mono text-xs font-bold tracking-[0.08em] opacity-85">Ticket #4821</span>
<b class="text-xl font-extrabold tracking-[-0.02em]">Payment retry loop</b>
<i class="mt-auto font-stk09-mono text-xs not-italic opacity-85">High · Billing</i>
</button>
<button class="absolute inset-0 flex flex-col gap-2 border-0 cursor-pointer rounded-[20px] bg-stk09-c2 p-6 text-left text-white shadow-[0_24px_46px_-26px_rgba(0,0,0,0.8)] [transform:translateY(calc(var(--depth,0)*16px))_scale(calc(1-var(--depth,0)*0.05))] [z-index:calc(10-var(--depth,0))] transition-[transform,opacity] duration-500 ease-stk09 data-[sending=true]:opacity-0 data-[sending=true]:duration-450 data-[sending=true]:ease-in data-[sending=true]:[transform:translateX(130%)_rotate(12deg)] focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-3 motion-reduce:duration-200" type="button">
<span class="font-stk09-mono text-xs font-bold tracking-[0.08em] opacity-85">Ticket #4822</span>
<b class="text-xl font-extrabold tracking-[-0.02em]">Slow search on mobile</b>
<i class="mt-auto font-stk09-mono text-xs not-italic opacity-85">Medium · Web</i>
</button>
<button class="absolute inset-0 flex flex-col gap-2 border-0 cursor-pointer rounded-[20px] bg-stk09-c3 p-6 text-left text-white shadow-[0_24px_46px_-26px_rgba(0,0,0,0.8)] [transform:translateY(calc(var(--depth,0)*16px))_scale(calc(1-var(--depth,0)*0.05))] [z-index:calc(10-var(--depth,0))] transition-[transform,opacity] duration-500 ease-stk09 data-[sending=true]:opacity-0 data-[sending=true]:duration-450 data-[sending=true]:ease-in data-[sending=true]:[transform:translateX(130%)_rotate(12deg)] focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-3 motion-reduce:duration-200" type="button">
<span class="font-stk09-mono text-xs font-bold tracking-[0.08em] opacity-85">Ticket #4823</span>
<b class="text-xl font-extrabold tracking-[-0.02em]">Export to CSV missing</b>
<i class="mt-auto font-stk09-mono text-xs not-italic opacity-85">Low · Reports</i>
</button>
<button class="absolute inset-0 flex flex-col gap-2 border-0 cursor-pointer rounded-[20px] bg-stk09-c4 p-6 text-left text-white shadow-[0_24px_46px_-26px_rgba(0,0,0,0.8)] [transform:translateY(calc(var(--depth,0)*16px))_scale(calc(1-var(--depth,0)*0.05))] [z-index:calc(10-var(--depth,0))] transition-[transform,opacity] duration-500 ease-stk09 data-[sending=true]:opacity-0 data-[sending=true]:duration-450 data-[sending=true]:ease-in data-[sending=true]:[transform:translateX(130%)_rotate(12deg)] focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-3 motion-reduce:duration-200" type="button">
<span class="font-stk09-mono text-xs font-bold tracking-[0.08em] opacity-85">Ticket #4824</span>
<b class="text-xl font-extrabold tracking-[-0.02em]">SSO redirect fails</b>
<i class="mt-auto font-stk09-mono text-xs not-italic opacity-85">High · Auth</i>
</button>
</div>
<div class="flex flex-col items-center gap-2">
<button class="rounded-xl border border-stk09-line bg-stk09-btn px-5.5 py-2.75 text-sm font-bold text-white cursor-pointer transition-colors duration-200 hover:bg-stk09-btnh focus-visible:outline-3 focus-visible:outline-stk09-ring focus-visible:outline-offset-2" id="stk-09-next" type="button">Next ticket ↻</button>
<p class="sr-only" id="stk-09-live" aria-live="polite"></p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-stk09-bg: #101219;
--color-stk09-bg: oklch(0.19 0.012 265);
--color-stk09-btn: #232838;
--color-stk09-btn: oklch(0.28 0.018 265);
--color-stk09-btnh: #2e3446;
--color-stk09-btnh: oklch(0.33 0.02 265);
--color-stk09-line: #333a4e;
--color-stk09-line: oklch(0.36 0.02 265);
--color-stk09-ring: #5aa2e0;
--color-stk09-ring: oklch(0.66 0.15 250);
--color-stk09-c1: #d2604c;
--color-stk09-c1: oklch(0.63 0.17 20);
--color-stk09-c2: #45ab7c;
--color-stk09-c2: oklch(0.66 0.15 150);
--color-stk09-c3: #4d8ade;
--color-stk09-c3: oklch(0.64 0.16 250);
--color-stk09-c4: #9b6ed6;
--color-stk09-c4: oklch(0.64 0.16 300);
--font-stk09-sans: 'Archivo', system-ui, sans-serif;
--font-stk09-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-stk09: cubic-bezier(0.3, 0.9, 0.3, 1);
}
```
## JavaScript
```js
(() => {
const root = document.querySelector('.stk-09');
if (!root) return;
const deck = root.querySelector('#stk-09-deck');
const live = root.querySelector('#stk-09-live');
/* The controller writes ONE integer per card. Translate, scale and z-index
are all derived from it by arbitrary calc() utilities in the markup. */
const paint = () => [...deck.children].forEach((c, i) => c.style.setProperty('--depth', i));
function cycle() {
const front = deck.firstElementChild;
if (!front) return;
front.dataset.sending = 'true';
front.addEventListener('transitionend', () => {
delete front.dataset.sending;
deck.appendChild(front);
paint();
const title = deck.firstElementChild.querySelector('b');
if (title) live.textContent = 'Now showing ' + title.textContent;
}, { once: true });
}
deck.addEventListener('click', (e) => {
if (e.target.closest('button') === deck.firstElementChild) cycle();
});
root.querySelector('#stk-09-next').addEventListener('click', cycle);
paint();
})();
```How this works
Depth is data. The controller sets --depth to each card's index and nothing else. Three arbitrary values turn that one number into the whole visual hierarchy:
[transform:translateY(calc(var(--depth)*16px))_scale(calc(1-var(--depth)*0.05))]
[z-index:calc(10-var(--depth))]Because depth is not DOM order, re-appending the flung card to the end of the deck does not fight the CSS — the repaint simply gives every card its new number and the transition does the rest.
The exit is a variant, the reorder is a script. data-[sending=true] holds the fling: a translate, a rotate, an opacity and a shorter ease-in. The script waits for transitionend before moving the node, which is the difference between a glide and a snap.
Cards are buttons, and that settles accessibility. No tabindex, no synthetic key handling, native Enter and Space. The only thing left to add is the announcement: an aria-live region naming whichever ticket is now on top, because a purely visual reshuffle tells a screen-reader user nothing.
Make it yours
- Change the fling by editing the
data-[sending=true]:[transform:…]value — direction, distance and rotation are all in one place. - Tighten or deepen the pile via the
16pxand0.05factors in the depth transform. - Add a Prev action by moving the last child to the front and repainting; the depth model needs no change.
- Recolour a card by editing its own
bg-stk09-*utility. - Add cards freely — the depth mapping is arithmetic, so the pile absorbs them without a new rule.
Gotchas — read before shipping
- Drive depth from a custom property, not DOM order. Re-appending a node while CSS keys off
:nth-childmakes the whole pile jump. - Wait for
transitionendbefore re-appending, or the card teleports instead of gliding. z-indexhas no calc-able utility.[z-index:calc(10-var(--depth))]is the correct arbitrary property, not a workaround.- Clear
data-sendingbefore the reorder, or the returning card keeps the fling transform and never comes back. - Keep the Next button. Click-the-front-card alone is a discoverability problem as much as an accessibility one.
- Announce the new front card. Without the
aria-liveregion the reshuffle is silent to assistive technology.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
Custom properties in calc(), transforms and transitionend are universal (Chrome 49+, Safari 10+, Firefox 52+). The floor is Tailwind v4's own generated sheet.