36 Tailwind Stacked Cards
24 / 36

All 36 in 36 Tailwind Stacked Cards

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

Tailwind Card Stack Tilt on Hover Quadrant

A deck that snaps to one of four poses depending on which quadrant your pointer is in — crisper and far cheaper than continuous tilt. The script writes two unitless numbers and CSS multiplies them into degrees, which is the pattern worth stealing: rotateX(calc(var(--ty,0)*1deg)) keeps the JS free of units and the CSS free of parsing.

Live Demo
Plain CSSTailwind v4
Try it

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

Untitled UI React · Affiliate link

Get Untitled UI Pro

The code

<section class="stk-24 w-full min-h-svh flex items-center justify-center px-5 py-15 font-stk24-sans bg-stk24-bg [background-image:radial-gradient(circle_at_50%_40%,var(--color-stk24-glow),transparent_70%)]" aria-label="Quadrant tilt card stack">
  <div class="w-[min(300px,90vw)] [perspective:900px] has-focus-visible:[--tx:-9] has-focus-visible:[--ty:7]">
    <div class="group relative h-55 outline-none [transform-style:preserve-3d] [transform:rotateX(calc(var(--ty,0)*1deg))_rotateY(calc(var(--tx,0)*1deg))] transition-transform duration-400 ease-out motion-reduce:transition-none" id="stk-24-stack" tabindex="0" role="group" aria-label="Tilting card deck">
      <div class="absolute inset-0 rounded-[20px] border border-stk24-line bg-stk24-card shadow-[0_22px_44px_-26px_rgba(0,0,0,0.85)] [transform:translate(calc(var(--i)*8px),calc(var(--i)*8px))_translateZ(calc(var(--i)*-20px))]" style="--i:2" aria-hidden="true"></div>
      <div class="absolute inset-0 rounded-[20px] border border-stk24-line bg-stk24-card shadow-[0_22px_44px_-26px_rgba(0,0,0,0.85)] [transform:translate(calc(var(--i)*8px),calc(var(--i)*8px))_translateZ(calc(var(--i)*-20px))]" style="--i:1" aria-hidden="true"></div>
      <div class="absolute inset-0 overflow-hidden rounded-[20px] border border-stk24-line p-7 text-stk24-ink bg-linear-[150deg,var(--color-stk24-t1),var(--color-stk24-t2)] shadow-[0_22px_44px_-26px_rgba(0,0,0,0.85)] [transform:translate(calc(var(--i)*8px),calc(var(--i)*8px))_translateZ(calc(var(--i)*-20px))]" style="--i:0">
        <p class="m-0 font-stk24-mono text-xs font-medium tracking-[0.16em] uppercase text-stk24-accent">Interactive</p>
        <h3 class="mt-2.5 mb-2 text-3xl font-extrabold tracking-[-0.02em]">Tilt me</h3>
        <p class="m-0 max-w-[26ch] text-sm leading-snug text-stk24-body">Enter from any corner &mdash; the deck banks toward you.</p>
        <span class="absolute inset-0 pointer-events-none opacity-0 transition-opacity duration-300 group-hover:opacity-100 group-focus-visible:opacity-100 [background-image:radial-gradient(200px_circle_at_var(--mx,50%)_var(--my,50%),rgba(255,255,255,0.22),transparent_60%)]" id="stk-24-sheen" aria-hidden="true"></span>
      </div>
    </div>
  </div>
</section>
@import "tailwindcss";

@theme {
  --color-stk24-bg: #080d12;
  --color-stk24-bg: oklch(0.13 0.014 220);
  --color-stk24-glow: #10222a;
  --color-stk24-glow: oklch(0.24 0.030 215);
  --color-stk24-card: #132831;
  --color-stk24-card: oklch(0.27 0.030 215);
  --color-stk24-line: #1e3b46;
  --color-stk24-line: oklch(0.34 0.035 215);
  --color-stk24-t1: #153039;
  --color-stk24-t1: oklch(0.29 0.032 215);
  --color-stk24-t2: #0e2028;
  --color-stk24-t2: oklch(0.22 0.028 215);
  --color-stk24-ink: #eaf6f8;
  --color-stk24-ink: oklch(0.96 0.012 215);
  --color-stk24-body: #a9c6cd;
  --color-stk24-body: oklch(0.80 0.028 215);
  --color-stk24-accent: #4fd0dc;
  --color-stk24-accent: oklch(0.80 0.13 195);
  --font-stk24-sans: 'Archivo', system-ui, sans-serif;
  --font-stk24-mono: 'JetBrains Mono', ui-monospace, monospace;
}
(() => {
  const root = document.querySelector('.stk-24');
  if (!root) return;
  const stack = root.querySelector('#stk-24-stack');
  const sheen = root.querySelector('#stk-24-sheen');
  const MAX = 12;

  /* Unitless numbers only. CSS multiplies them into degrees, so this handler
     never concatenates units and the same values could drive anything else. */
  stack.addEventListener('pointermove', (e) => {
    const r = stack.getBoundingClientRect();
    const px = (e.clientX - r.left) / r.width;
    const py = (e.clientY - r.top) / r.height;
    stack.style.setProperty('--tx', String((px < 0.5 ? -1 : 1) * MAX));
    stack.style.setProperty('--ty', String((py < 0.5 ? 1 : -1) * MAX));
    sheen.style.setProperty('--mx', (px * 100).toFixed(1) + '%');
    sheen.style.setProperty('--my', (py * 100).toFixed(1) + '%');
  });

  stack.addEventListener('pointerleave', () => {
    stack.style.setProperty('--tx', '0');
    stack.style.setProperty('--ty', '0');
  });
})();

Setup — do this once per project

Skip this if your project already runs Tailwind v4.

1Load Tailwind
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>

Compiles in the browser — perfect for a CodePen or a quick test, not for production.

2Add the stylesheetRequiredthe @theme block is what makes the custom utilities exist
@import "tailwindcss";

@theme {
  --color-stk24-bg: #080d12;
  --color-stk24-bg: oklch(0.13 0.014 220);
  --color-stk24-glow: #10222a;
  --color-stk24-glow: oklch(0.24 0.030 215);
  --color-stk24-card: #132831;
  --color-stk24-card: oklch(0.27 0.030 215);
  --color-stk24-line: #1e3b46;
  --color-stk24-line: oklch(0.34 0.035 215);
  --color-stk24-t1: #153039;
  --color-stk24-t1: oklch(0.29 0.032 215);
  --color-stk24-t2: #0e2028;
  --color-stk24-t2: oklch(0.22 0.028 215);
  --color-stk24-ink: #eaf6f8;
  --color-stk24-ink: oklch(0.96 0.012 215);
  --color-stk24-body: #a9c6cd;
  --color-stk24-body: oklch(0.80 0.028 215);
  --color-stk24-accent: #4fd0dc;
  --color-stk24-accent: oklch(0.80 0.13 195);
  --font-stk24-sans: 'Archivo', system-ui, sans-serif;
  --font-stk24-mono: 'JetBrains Mono', ui-monospace, monospace;
}
3Fonts used by this demooptional — skip if you have your own type stack
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=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 in setup, not as an @import inside the stylesheet. Tailwind emits user @import rules after its own @layer blocks, and a late @import is invalid CSS — the browser drops it and the demo silently falls back to system fonts.
  • The handler writes unitless numbers and the utility multiplies them into degrees — calc(var(--ty,0)*1deg) — which keeps units out of the JavaScript entirely.
  • The var(--tx,0) fallbacks live in the class list, so the deck renders flat and correct before the script has run.
  • Quadrant snapping is a better interaction as well as a cheaper one: four stable poses, no jitter at the midlines.
  • The keyboard pose moves from a :has() rule to has-focus-visible:[--tx:-9] on the scene — a variant setting a custom property, which is unusual and exactly right here.
  • The sheen is one span with an arbitrary radial-gradient and pointer-events-none; the CSS version needs a rule plus a second rule to reveal it.
  • Depth on the under-cards combines a 2D offset and a translateZ from the same index, so the pile has thickness in the tilt rather than just an offset.
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind Card Stack Tilt on Hover Quadrant
Source: https://codefronts.com/components/tailwind-stacked-cards/tailwind-card-stack-tilt-on-hover-quadrant/

A deck that snaps to one of four poses depending on which quadrant your pointer is in &mdash; crisper and far cheaper than continuous tilt. The script writes two unitless numbers and CSS multiplies them into degrees, which is the pattern worth stealing: rotateX(calc(var(--ty,0)*1deg)) keeps the JS free of units and the CSS free of parsing.
## HTML
```html
<section class="stk-24 w-full min-h-svh flex items-center justify-center px-5 py-15 font-stk24-sans bg-stk24-bg [background-image:radial-gradient(circle_at_50%_40%,var(--color-stk24-glow),transparent_70%)]" aria-label="Quadrant tilt card stack">
  <div class="w-[min(300px,90vw)] [perspective:900px] has-focus-visible:[--tx:-9] has-focus-visible:[--ty:7]">
    <div class="group relative h-55 outline-none [transform-style:preserve-3d] [transform:rotateX(calc(var(--ty,0)*1deg))_rotateY(calc(var(--tx,0)*1deg))] transition-transform duration-400 ease-out motion-reduce:transition-none" id="stk-24-stack" tabindex="0" role="group" aria-label="Tilting card deck">
      <div class="absolute inset-0 rounded-[20px] border border-stk24-line bg-stk24-card shadow-[0_22px_44px_-26px_rgba(0,0,0,0.85)] [transform:translate(calc(var(--i)*8px),calc(var(--i)*8px))_translateZ(calc(var(--i)*-20px))]" style="--i:2" aria-hidden="true"></div>
      <div class="absolute inset-0 rounded-[20px] border border-stk24-line bg-stk24-card shadow-[0_22px_44px_-26px_rgba(0,0,0,0.85)] [transform:translate(calc(var(--i)*8px),calc(var(--i)*8px))_translateZ(calc(var(--i)*-20px))]" style="--i:1" aria-hidden="true"></div>
      <div class="absolute inset-0 overflow-hidden rounded-[20px] border border-stk24-line p-7 text-stk24-ink bg-linear-[150deg,var(--color-stk24-t1),var(--color-stk24-t2)] shadow-[0_22px_44px_-26px_rgba(0,0,0,0.85)] [transform:translate(calc(var(--i)*8px),calc(var(--i)*8px))_translateZ(calc(var(--i)*-20px))]" style="--i:0">
        <p class="m-0 font-stk24-mono text-xs font-medium tracking-[0.16em] uppercase text-stk24-accent">Interactive</p>
        <h3 class="mt-2.5 mb-2 text-3xl font-extrabold tracking-[-0.02em]">Tilt me</h3>
        <p class="m-0 max-w-[26ch] text-sm leading-snug text-stk24-body">Enter from any corner &mdash; the deck banks toward you.</p>
        <span class="absolute inset-0 pointer-events-none opacity-0 transition-opacity duration-300 group-hover:opacity-100 group-focus-visible:opacity-100 [background-image:radial-gradient(200px_circle_at_var(--mx,50%)_var(--my,50%),rgba(255,255,255,0.22),transparent_60%)]" id="stk-24-sheen" aria-hidden="true"></span>
      </div>
    </div>
  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-stk24-bg: #080d12;
  --color-stk24-bg: oklch(0.13 0.014 220);
  --color-stk24-glow: #10222a;
  --color-stk24-glow: oklch(0.24 0.030 215);
  --color-stk24-card: #132831;
  --color-stk24-card: oklch(0.27 0.030 215);
  --color-stk24-line: #1e3b46;
  --color-stk24-line: oklch(0.34 0.035 215);
  --color-stk24-t1: #153039;
  --color-stk24-t1: oklch(0.29 0.032 215);
  --color-stk24-t2: #0e2028;
  --color-stk24-t2: oklch(0.22 0.028 215);
  --color-stk24-ink: #eaf6f8;
  --color-stk24-ink: oklch(0.96 0.012 215);
  --color-stk24-body: #a9c6cd;
  --color-stk24-body: oklch(0.80 0.028 215);
  --color-stk24-accent: #4fd0dc;
  --color-stk24-accent: oklch(0.80 0.13 195);
  --font-stk24-sans: 'Archivo', system-ui, sans-serif;
  --font-stk24-mono: 'JetBrains Mono', ui-monospace, monospace;
}
```

## JavaScript
```js
(() => {
  const root = document.querySelector('.stk-24');
  if (!root) return;
  const stack = root.querySelector('#stk-24-stack');
  const sheen = root.querySelector('#stk-24-sheen');
  const MAX = 12;

  /* Unitless numbers only. CSS multiplies them into degrees, so this handler
     never concatenates units and the same values could drive anything else. */
  stack.addEventListener('pointermove', (e) => {
    const r = stack.getBoundingClientRect();
    const px = (e.clientX - r.left) / r.width;
    const py = (e.clientY - r.top) / r.height;
    stack.style.setProperty('--tx', String((px < 0.5 ? -1 : 1) * MAX));
    stack.style.setProperty('--ty', String((py < 0.5 ? 1 : -1) * MAX));
    sheen.style.setProperty('--mx', (px * 100).toFixed(1) + '%');
    sheen.style.setProperty('--my', (py * 100).toFixed(1) + '%');
  });

  stack.addEventListener('pointerleave', () => {
    stack.style.setProperty('--tx', '0');
    stack.style.setProperty('--ty', '0');
  });
})();
```

How this works

Let CSS own the units. The handler writes --tx and --ty as bare numbers and the transform gives them meaning:

[transform:rotateX(calc(var(--ty,0)*1deg))_rotateY(calc(var(--tx,0)*1deg))]

Two benefits, both real: the script never does string concatenation with units, and the same numbers could later drive a translate or a shadow without being re-parsed. The fallbacks in var(--ty,0) mean the deck renders flat before the script runs.

Quadrants, not continuous tracking. Rounding the pointer position to a sign per axis gives four poses. It costs one comparison instead of a smooth interpolation, it never jitters at the edges, and it reads as deliberate rather than clingy — a genuinely better interaction, not just a cheaper one.

The sheen is the only continuous part. --mx/--my track the pointer as percentages and feed one arbitrary radial-gradient. Because it is a background and not a layout property, it stays on the compositor and does not fight the snapping tilt.

Keyboards get a pose, not nothing. has-focus-visible: on the scene applies a fixed flattering tilt, so tabbing to the deck shows the same 3D affordance a pointer user gets.

Make it yours

  • Change the tilt ceiling with the MAX constant — 12deg is deliberate; past about 18deg the text starts to distort.
  • Switch to continuous tilt by removing the sign rounding in the handler; the CSS needs no change at all.
  • Restyle or drop the sheen — it is one element and one background.
  • Recolour through --color-stk24-accent.
  • Change the pile's thickness with the 8px offsets and the -20px translateZ step.

Gotchas — read before shipping

  • Write custom properties in the pointer handler, never read geometry there. A getBoundingClientRect() per pointermove forces layout and shows up in INP.
  • Reset on pointerleave, or the tilt sticks at whatever pose the pointer left behind.
  • [perspective:…] belongs on the scene, not on the tilting stack.
  • Include the var(--tx,0) fallbacks so the deck renders flat rather than broken before the script runs.
  • The sheen must be pointer-events-none or it swallows the hover it exists to visualise.
  • Cap the tilt. Beyond about 18deg the text on the top card is visibly distorted and harder to read.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

3D transforms and custom properties in calc() are broadly supported (Chrome 36+, Safari 9+, Firefox 16+). The floor is Tailwind v4's own generated sheet.

Techniques used in this demo

Theme tokensset in @theme, used by a utility

Arbitraryneeds bracket syntax

Search CodeFronts

Loading…