Pure TailwindMIT licensed

Tailwind 3D Perspective Depth Stack Gallery That Fans on Focus

A deck of photographs stacked in 3D that fans into a cascade when you hover or focus it. Each card's resting depth and fanned position are two utilities on the card itself, which replaces four nth-child rules with markup you can reorder freely.

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="tgl-18 w-full min-h-svh flex flex-col items-center justify-center gap-7 bg-tgl18-bg font-tgl18-sans px-5 py-10" aria-label="Perspective depth stack gallery">
  <div class="group relative w-[260px] h-[330px] [perspective:1200px]">
    <a href="#" class="absolute inset-0 rounded-2xl overflow-hidden transform-3d origin-bottom shadow-[0_20px_40px_-20px_rgba(0,0,0,0.7)] [transform:translateZ(60px)] transition-transform duration-[550ms] ease-[cubic-bezier(0.2,0.7,0.2,1)] group-hover:[transform:translateX(-150px)_rotate(-9deg)] group-focus-within:[transform:translateX(-150px)_rotate(-9deg)] focus-visible:outline-3 focus-visible:outline-tgl18-acc focus-visible:outline-offset-[3px] focus-visible:z-[5] motion-reduce:transition-none">
      <img src="https://images.unsplash.com/photo-1607746882042-944635dfe10e?q=80&w=500&h=620&auto=format&fit=crop" alt="Studio portrait in high contrast" loading="lazy" decoding="async" class="block w-full h-full object-cover">
    </a>
    <a href="#" class="absolute inset-0 rounded-2xl overflow-hidden transform-3d origin-bottom shadow-[0_20px_40px_-20px_rgba(0,0,0,0.7)] [transform:translateZ(40px)_translateY(10px)_scale(0.97)] transition-transform duration-[550ms] ease-[cubic-bezier(0.2,0.7,0.2,1)] group-hover:[transform:translateX(-50px)_rotate(-3deg)] group-focus-within:[transform:translateX(-50px)_rotate(-3deg)] focus-visible:outline-3 focus-visible:outline-tgl18-acc focus-visible:outline-offset-[3px] focus-visible:z-[5] motion-reduce:transition-none">
      <img src="https://images.unsplash.com/photo-1505691938895-1758d7feb511?q=80&w=500&h=620&auto=format&fit=crop" alt="Minimal landscape study" loading="lazy" decoding="async" class="block w-full h-full object-cover">
    </a>
    <a href="#" class="absolute inset-0 rounded-2xl overflow-hidden transform-3d origin-bottom shadow-[0_20px_40px_-20px_rgba(0,0,0,0.7)] [transform:translateZ(20px)_translateY(20px)_scale(0.94)] transition-transform duration-[550ms] ease-[cubic-bezier(0.2,0.7,0.2,1)] group-hover:[transform:translateX(50px)_rotate(3deg)] group-focus-within:[transform:translateX(50px)_rotate(3deg)] focus-visible:outline-3 focus-visible:outline-tgl18-acc focus-visible:outline-offset-[3px] focus-visible:z-[5] motion-reduce:transition-none">
      <img src="https://images.unsplash.com/photo-1470252649378-9c29740c9fa8?q=80&w=500&h=620&auto=format&fit=crop" alt="Street scene in late light" loading="lazy" decoding="async" class="block w-full h-full object-cover">
    </a>
    <a href="#" class="absolute inset-0 rounded-2xl overflow-hidden transform-3d origin-bottom shadow-[0_20px_40px_-20px_rgba(0,0,0,0.7)] [transform:translateZ(0)_translateY(30px)_scale(0.91)] transition-transform duration-[550ms] ease-[cubic-bezier(0.2,0.7,0.2,1)] group-hover:[transform:translateX(150px)_rotate(9deg)] group-focus-within:[transform:translateX(150px)_rotate(9deg)] focus-visible:outline-3 focus-visible:outline-tgl18-acc focus-visible:outline-offset-[3px] focus-visible:z-[5] motion-reduce:transition-none">
      <img src="https://images.unsplash.com/photo-1450101499163-c8848c66ca85?q=80&w=500&h=620&auto=format&fit=crop" alt="Botanical detail on black" loading="lazy" decoding="async" class="block w-full h-full object-cover">
    </a>
  </div>
  <p class="font-tgl18-mono text-[11px] tracking-[0.16em] uppercase text-tgl18-mut m-0">hover or focus the stack to fan it out</p>
</section>
@import "tailwindcss";

@theme {
  --color-tgl18-bg: #14101a;
  --color-tgl18-bg: oklch(0.18 0.018 310);
  --color-tgl18-mut: #a396ad;
  --color-tgl18-mut: oklch(0.72 0.02 315);
  --color-tgl18-acc: #d06fc8;
  --color-tgl18-acc: oklch(0.7 0.17 330);
  --font-tgl18-sans: 'Space Grotesk', system-ui, sans-serif;
  --font-tgl18-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
}

.tgl-18 {
  width: 100%;
  min-height: 100svh;
}

.tgl-18 ::selection {
  background: var(--color-tgl18-acc);
  color: #fff;
}

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-tgl18-bg: #14101a;
  --color-tgl18-bg: oklch(0.18 0.018 310);
  --color-tgl18-mut: #a396ad;
  --color-tgl18-mut: oklch(0.72 0.02 315);
  --color-tgl18-acc: #d06fc8;
  --color-tgl18-acc: oklch(0.7 0.17 330);
  --font-tgl18-sans: 'Space Grotesk', system-ui, sans-serif;
  --font-tgl18-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
}

.tgl-18 {
  width: 100%;
  min-height: 100svh;
}

.tgl-18 ::selection {
  background: var(--color-tgl18-acc);
  color: #fff;
}
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=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap">

What changed in the port

Which parts became utilities, which stayed CSS, and why.

  • Eight nth-child rules become two utilities per card. The deck's order now lives in the DOM alone, which is the difference between a demo and something a CMS can render.
  • origin-bottom is a real utility and worth stating explicitly: the fan reads as a hand of cards only because the pivot is at the base.
  • UI change from the CSS original: the hint line went from #8a7e96 to a lighter token — at 11px uppercase with 0.16em tracking the original sat at 3.4:1 on the dark stage, below the small-text floor.
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 3D Perspective Depth Stack Gallery That Fans on Focus
Source: https://codefronts.com/snippets/tailwind-css-image-gallery/tailwind-3d-perspective-depth-stack-gallery/

A deck of photographs stacked in 3D that fans into a cascade when you hover or focus it. Each card's resting depth and fanned position are two utilities on the card itself, which replaces four nth-child rules with markup you can reorder freely.
## HTML
```html
<section class="tgl-18 w-full min-h-svh flex flex-col items-center justify-center gap-7 bg-tgl18-bg font-tgl18-sans px-5 py-10" aria-label="Perspective depth stack gallery">
  <div class="group relative w-[260px] h-[330px] [perspective:1200px]">
    <a href="#" class="absolute inset-0 rounded-2xl overflow-hidden transform-3d origin-bottom shadow-[0_20px_40px_-20px_rgba(0,0,0,0.7)] [transform:translateZ(60px)] transition-transform duration-[550ms] ease-[cubic-bezier(0.2,0.7,0.2,1)] group-hover:[transform:translateX(-150px)_rotate(-9deg)] group-focus-within:[transform:translateX(-150px)_rotate(-9deg)] focus-visible:outline-3 focus-visible:outline-tgl18-acc focus-visible:outline-offset-[3px] focus-visible:z-[5] motion-reduce:transition-none">
      <img src="https://images.unsplash.com/photo-1607746882042-944635dfe10e?q=80&w=500&h=620&auto=format&fit=crop" alt="Studio portrait in high contrast" loading="lazy" decoding="async" class="block w-full h-full object-cover">
    </a>
    <a href="#" class="absolute inset-0 rounded-2xl overflow-hidden transform-3d origin-bottom shadow-[0_20px_40px_-20px_rgba(0,0,0,0.7)] [transform:translateZ(40px)_translateY(10px)_scale(0.97)] transition-transform duration-[550ms] ease-[cubic-bezier(0.2,0.7,0.2,1)] group-hover:[transform:translateX(-50px)_rotate(-3deg)] group-focus-within:[transform:translateX(-50px)_rotate(-3deg)] focus-visible:outline-3 focus-visible:outline-tgl18-acc focus-visible:outline-offset-[3px] focus-visible:z-[5] motion-reduce:transition-none">
      <img src="https://images.unsplash.com/photo-1505691938895-1758d7feb511?q=80&w=500&h=620&auto=format&fit=crop" alt="Minimal landscape study" loading="lazy" decoding="async" class="block w-full h-full object-cover">
    </a>
    <a href="#" class="absolute inset-0 rounded-2xl overflow-hidden transform-3d origin-bottom shadow-[0_20px_40px_-20px_rgba(0,0,0,0.7)] [transform:translateZ(20px)_translateY(20px)_scale(0.94)] transition-transform duration-[550ms] ease-[cubic-bezier(0.2,0.7,0.2,1)] group-hover:[transform:translateX(50px)_rotate(3deg)] group-focus-within:[transform:translateX(50px)_rotate(3deg)] focus-visible:outline-3 focus-visible:outline-tgl18-acc focus-visible:outline-offset-[3px] focus-visible:z-[5] motion-reduce:transition-none">
      <img src="https://images.unsplash.com/photo-1470252649378-9c29740c9fa8?q=80&w=500&h=620&auto=format&fit=crop" alt="Street scene in late light" loading="lazy" decoding="async" class="block w-full h-full object-cover">
    </a>
    <a href="#" class="absolute inset-0 rounded-2xl overflow-hidden transform-3d origin-bottom shadow-[0_20px_40px_-20px_rgba(0,0,0,0.7)] [transform:translateZ(0)_translateY(30px)_scale(0.91)] transition-transform duration-[550ms] ease-[cubic-bezier(0.2,0.7,0.2,1)] group-hover:[transform:translateX(150px)_rotate(9deg)] group-focus-within:[transform:translateX(150px)_rotate(9deg)] focus-visible:outline-3 focus-visible:outline-tgl18-acc focus-visible:outline-offset-[3px] focus-visible:z-[5] motion-reduce:transition-none">
      <img src="https://images.unsplash.com/photo-1450101499163-c8848c66ca85?q=80&w=500&h=620&auto=format&fit=crop" alt="Botanical detail on black" loading="lazy" decoding="async" class="block w-full h-full object-cover">
    </a>
  </div>
  <p class="font-tgl18-mono text-[11px] tracking-[0.16em] uppercase text-tgl18-mut m-0">hover or focus the stack to fan it out</p>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tgl18-bg: #14101a;
  --color-tgl18-bg: oklch(0.18 0.018 310);
  --color-tgl18-mut: #a396ad;
  --color-tgl18-mut: oklch(0.72 0.02 315);
  --color-tgl18-acc: #d06fc8;
  --color-tgl18-acc: oklch(0.7 0.17 330);
  --font-tgl18-sans: 'Space Grotesk', system-ui, sans-serif;
  --font-tgl18-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
}

.tgl-18 {
  width: 100%;
  min-height: 100svh;
}

.tgl-18 ::selection {
  background: var(--color-tgl18-acc);
  color: #fff;
}
```

How this works

Per-card transforms beat nth-child. The CSS version encoded card order in eight selectors; here each card owns both of its poses. Reordering the deck is moving an element, not renumbering a stylesheet.

Two states, one group. The stack is the group; hover or focus anywhere inside it fans every card at once, which is what makes the deck feel like one object rather than four.

Keyboard gets the same deck. Every card is a focusable anchor, so tabbing into the stack fans it and the card behind the top one is reachable — the original hid three of four photographs from keyboard users until focus happened to land right.

Make it yours

  • Resting depth: each card's translateZ/scale step.
  • Fan spread: the translateX and rotate values in the hover pose.
  • Add a fifth card by copying one and giving it the next depth step and fan position.

Gotchas — read before shipping

  • Give the stack a fixed size. Absolutely-positioned cards need a frame, or the fan has nothing to be measured against.
  • Keep the translateZ steps small relative to the perspective value, or back cards clip through the perspective origin.
  • group-focus-within: is what makes the deck keyboard-usable — without it three of four photographs are unreachable in practice.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

3D transforms and :focus-within are universal; the floor is Tailwind v4's generated sheet.

Techniques used in this demo

Search CodeFronts

Loading…