05 / 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 Fan Out Photo Stack on Hover
A tidy pile that fans into an arc, where the arithmetic replaces the rule. Every photo carries --n — its signed position from the centre — and one shared class list turns that into both the rotation and the arc lift: group-hover:[transform:rotate(calc(var(--n)*15deg))_translateY(calc(var(--n)*var(--n)*-2px))]. Five identical class lists, five different destinations, no :nth-child map anywhere.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="stk-05 w-full min-h-svh flex flex-col items-center justify-center gap-[34px] px-5 py-15 font-stk05-sans bg-stk05-paper" aria-label="Fan-out photo stack">
<div class="group relative w-55 h-75">
<a class="absolute left-1/2 top-0 -ml-[90px] w-45 rounded-[14px] overflow-hidden bg-white p-2 pb-[22px] shadow-[0_16px_34px_-20px_rgba(60,40,20,0.6)] [transform-origin:50%_130%] transition-transform duration-500 ease-stk05 delay-0 group-hover:[transform:rotate(calc(var(--n)*15deg))_translateY(calc(var(--n)*var(--n)*-2px))] group-focus-within:[transform:rotate(calc(var(--n)*15deg))_translateY(calc(var(--n)*var(--n)*-2px))] hover:z-5 hover:[transform:rotate(calc(var(--n)*15deg))_translateY(-24px)]! focus-visible:z-5 focus-visible:outline-none focus-visible:shadow-[0_0_0_3px_var(--color-stk05-focus),0_20px_40px_-18px_rgba(60,40,20,0.7)] focus-visible:[transform:rotate(calc(var(--n)*15deg))_translateY(-24px)]! motion-reduce:duration-200 motion-reduce:group-hover:[transform:translateX(calc(var(--n)*48px))] motion-reduce:group-focus-within:[transform:translateX(calc(var(--n)*48px))]" href="#" style="--n:-2">
<img class="block w-full aspect-[3/4] object-cover rounded-lg" src="https://picsum.photos/seed/tw-fan-1/300/380" width="300" height="380" alt="Coastline seen from a cliff path">
</a>
<a class="absolute left-1/2 top-0 -ml-[90px] w-45 rounded-[14px] overflow-hidden bg-white p-2 pb-[22px] shadow-[0_16px_34px_-20px_rgba(60,40,20,0.6)] [transform-origin:50%_130%] transition-transform duration-500 ease-stk05 delay-[40ms] group-hover:[transform:rotate(calc(var(--n)*15deg))_translateY(calc(var(--n)*var(--n)*-2px))] group-focus-within:[transform:rotate(calc(var(--n)*15deg))_translateY(calc(var(--n)*var(--n)*-2px))] hover:z-5 hover:[transform:rotate(calc(var(--n)*15deg))_translateY(-24px)]! focus-visible:z-5 focus-visible:outline-none focus-visible:shadow-[0_0_0_3px_var(--color-stk05-focus),0_20px_40px_-18px_rgba(60,40,20,0.7)] focus-visible:[transform:rotate(calc(var(--n)*15deg))_translateY(-24px)]! motion-reduce:duration-200 motion-reduce:group-hover:[transform:translateX(calc(var(--n)*48px))] motion-reduce:group-focus-within:[transform:translateX(calc(var(--n)*48px))]" href="#" style="--n:-1">
<img class="block w-full aspect-[3/4] object-cover rounded-lg" src="https://picsum.photos/seed/tw-fan-2/300/380" width="300" height="380" alt="Narrow street in an old town">
</a>
<a class="absolute left-1/2 top-0 -ml-[90px] w-45 rounded-[14px] overflow-hidden bg-white p-2 pb-[22px] shadow-[0_16px_34px_-20px_rgba(60,40,20,0.6)] [transform-origin:50%_130%] transition-transform duration-500 ease-stk05 delay-[80ms] group-hover:[transform:rotate(calc(var(--n)*15deg))_translateY(calc(var(--n)*var(--n)*-2px))] group-focus-within:[transform:rotate(calc(var(--n)*15deg))_translateY(calc(var(--n)*var(--n)*-2px))] hover:z-5 hover:[transform:rotate(calc(var(--n)*15deg))_translateY(-24px)]! focus-visible:z-5 focus-visible:outline-none focus-visible:shadow-[0_0_0_3px_var(--color-stk05-focus),0_20px_40px_-18px_rgba(60,40,20,0.7)] focus-visible:[transform:rotate(calc(var(--n)*15deg))_translateY(-24px)]! motion-reduce:duration-200 motion-reduce:group-hover:[transform:translateX(calc(var(--n)*48px))] motion-reduce:group-focus-within:[transform:translateX(calc(var(--n)*48px))]" href="#" style="--n:0">
<img class="block w-full aspect-[3/4] object-cover rounded-lg" src="https://picsum.photos/seed/tw-fan-3/300/380" width="300" height="380" alt="Stone hut on a mountain ridge">
</a>
<a class="absolute left-1/2 top-0 -ml-[90px] w-45 rounded-[14px] overflow-hidden bg-white p-2 pb-[22px] shadow-[0_16px_34px_-20px_rgba(60,40,20,0.6)] [transform-origin:50%_130%] transition-transform duration-500 ease-stk05 delay-[40ms] group-hover:[transform:rotate(calc(var(--n)*15deg))_translateY(calc(var(--n)*var(--n)*-2px))] group-focus-within:[transform:rotate(calc(var(--n)*15deg))_translateY(calc(var(--n)*var(--n)*-2px))] hover:z-5 hover:[transform:rotate(calc(var(--n)*15deg))_translateY(-24px)]! focus-visible:z-5 focus-visible:outline-none focus-visible:shadow-[0_0_0_3px_var(--color-stk05-focus),0_20px_40px_-18px_rgba(60,40,20,0.7)] focus-visible:[transform:rotate(calc(var(--n)*15deg))_translateY(-24px)]! motion-reduce:duration-200 motion-reduce:group-hover:[transform:translateX(calc(var(--n)*48px))] motion-reduce:group-focus-within:[transform:translateX(calc(var(--n)*48px))]" href="#" style="--n:1">
<img class="block w-full aspect-[3/4] object-cover rounded-lg" src="https://picsum.photos/seed/tw-fan-4/300/380" width="300" height="380" alt="Harbour at dusk with moored boats">
</a>
<a class="absolute left-1/2 top-0 -ml-[90px] w-45 rounded-[14px] overflow-hidden bg-white p-2 pb-[22px] shadow-[0_16px_34px_-20px_rgba(60,40,20,0.6)] [transform-origin:50%_130%] transition-transform duration-500 ease-stk05 delay-0 group-hover:[transform:rotate(calc(var(--n)*15deg))_translateY(calc(var(--n)*var(--n)*-2px))] group-focus-within:[transform:rotate(calc(var(--n)*15deg))_translateY(calc(var(--n)*var(--n)*-2px))] hover:z-5 hover:[transform:rotate(calc(var(--n)*15deg))_translateY(-24px)]! focus-visible:z-5 focus-visible:outline-none focus-visible:shadow-[0_0_0_3px_var(--color-stk05-focus),0_20px_40px_-18px_rgba(60,40,20,0.7)] focus-visible:[transform:rotate(calc(var(--n)*15deg))_translateY(-24px)]! motion-reduce:duration-200 motion-reduce:group-hover:[transform:translateX(calc(var(--n)*48px))] motion-reduce:group-focus-within:[transform:translateX(calc(var(--n)*48px))]" href="#" style="--n:2">
<img class="block w-full aspect-[3/4] object-cover rounded-lg" src="https://picsum.photos/seed/tw-fan-5/300/380" width="300" height="380" alt="Rippled desert dunes at midday">
</a>
</div>
<p class="m-0 font-stk05-mono text-[11px] tracking-[0.14em] uppercase text-stk05-muted">Hover to fan · Tab to spread</p>
</section><section class="stk-05 w-full min-h-svh flex flex-col items-center justify-center gap-[34px] px-5 py-15 font-stk05-sans bg-stk05-paper" aria-label="Fan-out photo stack">
<div class="group relative w-55 h-75">
<a class="absolute left-1/2 top-0 -ml-[90px] w-45 rounded-[14px] overflow-hidden bg-white p-2 pb-[22px] shadow-[0_16px_34px_-20px_rgba(60,40,20,0.6)] [transform-origin:50%_130%] transition-transform duration-500 ease-stk05 delay-0 group-hover:[transform:rotate(calc(var(--n)*15deg))_translateY(calc(var(--n)*var(--n)*-2px))] group-focus-within:[transform:rotate(calc(var(--n)*15deg))_translateY(calc(var(--n)*var(--n)*-2px))] hover:z-5 hover:[transform:rotate(calc(var(--n)*15deg))_translateY(-24px)]! focus-visible:z-5 focus-visible:outline-none focus-visible:shadow-[0_0_0_3px_var(--color-stk05-focus),0_20px_40px_-18px_rgba(60,40,20,0.7)] focus-visible:[transform:rotate(calc(var(--n)*15deg))_translateY(-24px)]! motion-reduce:duration-200 motion-reduce:group-hover:[transform:translateX(calc(var(--n)*48px))] motion-reduce:group-focus-within:[transform:translateX(calc(var(--n)*48px))]" href="#" style="--n:-2">
<img class="block w-full aspect-[3/4] object-cover rounded-lg" src="https://picsum.photos/seed/tw-fan-1/300/380" width="300" height="380" alt="Coastline seen from a cliff path">
</a>
<a class="absolute left-1/2 top-0 -ml-[90px] w-45 rounded-[14px] overflow-hidden bg-white p-2 pb-[22px] shadow-[0_16px_34px_-20px_rgba(60,40,20,0.6)] [transform-origin:50%_130%] transition-transform duration-500 ease-stk05 delay-[40ms] group-hover:[transform:rotate(calc(var(--n)*15deg))_translateY(calc(var(--n)*var(--n)*-2px))] group-focus-within:[transform:rotate(calc(var(--n)*15deg))_translateY(calc(var(--n)*var(--n)*-2px))] hover:z-5 hover:[transform:rotate(calc(var(--n)*15deg))_translateY(-24px)]! focus-visible:z-5 focus-visible:outline-none focus-visible:shadow-[0_0_0_3px_var(--color-stk05-focus),0_20px_40px_-18px_rgba(60,40,20,0.7)] focus-visible:[transform:rotate(calc(var(--n)*15deg))_translateY(-24px)]! motion-reduce:duration-200 motion-reduce:group-hover:[transform:translateX(calc(var(--n)*48px))] motion-reduce:group-focus-within:[transform:translateX(calc(var(--n)*48px))]" href="#" style="--n:-1">
<img class="block w-full aspect-[3/4] object-cover rounded-lg" src="https://picsum.photos/seed/tw-fan-2/300/380" width="300" height="380" alt="Narrow street in an old town">
</a>
<a class="absolute left-1/2 top-0 -ml-[90px] w-45 rounded-[14px] overflow-hidden bg-white p-2 pb-[22px] shadow-[0_16px_34px_-20px_rgba(60,40,20,0.6)] [transform-origin:50%_130%] transition-transform duration-500 ease-stk05 delay-[80ms] group-hover:[transform:rotate(calc(var(--n)*15deg))_translateY(calc(var(--n)*var(--n)*-2px))] group-focus-within:[transform:rotate(calc(var(--n)*15deg))_translateY(calc(var(--n)*var(--n)*-2px))] hover:z-5 hover:[transform:rotate(calc(var(--n)*15deg))_translateY(-24px)]! focus-visible:z-5 focus-visible:outline-none focus-visible:shadow-[0_0_0_3px_var(--color-stk05-focus),0_20px_40px_-18px_rgba(60,40,20,0.7)] focus-visible:[transform:rotate(calc(var(--n)*15deg))_translateY(-24px)]! motion-reduce:duration-200 motion-reduce:group-hover:[transform:translateX(calc(var(--n)*48px))] motion-reduce:group-focus-within:[transform:translateX(calc(var(--n)*48px))]" href="#" style="--n:0">
<img class="block w-full aspect-[3/4] object-cover rounded-lg" src="https://picsum.photos/seed/tw-fan-3/300/380" width="300" height="380" alt="Stone hut on a mountain ridge">
</a>
<a class="absolute left-1/2 top-0 -ml-[90px] w-45 rounded-[14px] overflow-hidden bg-white p-2 pb-[22px] shadow-[0_16px_34px_-20px_rgba(60,40,20,0.6)] [transform-origin:50%_130%] transition-transform duration-500 ease-stk05 delay-[40ms] group-hover:[transform:rotate(calc(var(--n)*15deg))_translateY(calc(var(--n)*var(--n)*-2px))] group-focus-within:[transform:rotate(calc(var(--n)*15deg))_translateY(calc(var(--n)*var(--n)*-2px))] hover:z-5 hover:[transform:rotate(calc(var(--n)*15deg))_translateY(-24px)]! focus-visible:z-5 focus-visible:outline-none focus-visible:shadow-[0_0_0_3px_var(--color-stk05-focus),0_20px_40px_-18px_rgba(60,40,20,0.7)] focus-visible:[transform:rotate(calc(var(--n)*15deg))_translateY(-24px)]! motion-reduce:duration-200 motion-reduce:group-hover:[transform:translateX(calc(var(--n)*48px))] motion-reduce:group-focus-within:[transform:translateX(calc(var(--n)*48px))]" href="#" style="--n:1">
<img class="block w-full aspect-[3/4] object-cover rounded-lg" src="https://picsum.photos/seed/tw-fan-4/300/380" width="300" height="380" alt="Harbour at dusk with moored boats">
</a>
<a class="absolute left-1/2 top-0 -ml-[90px] w-45 rounded-[14px] overflow-hidden bg-white p-2 pb-[22px] shadow-[0_16px_34px_-20px_rgba(60,40,20,0.6)] [transform-origin:50%_130%] transition-transform duration-500 ease-stk05 delay-0 group-hover:[transform:rotate(calc(var(--n)*15deg))_translateY(calc(var(--n)*var(--n)*-2px))] group-focus-within:[transform:rotate(calc(var(--n)*15deg))_translateY(calc(var(--n)*var(--n)*-2px))] hover:z-5 hover:[transform:rotate(calc(var(--n)*15deg))_translateY(-24px)]! focus-visible:z-5 focus-visible:outline-none focus-visible:shadow-[0_0_0_3px_var(--color-stk05-focus),0_20px_40px_-18px_rgba(60,40,20,0.7)] focus-visible:[transform:rotate(calc(var(--n)*15deg))_translateY(-24px)]! motion-reduce:duration-200 motion-reduce:group-hover:[transform:translateX(calc(var(--n)*48px))] motion-reduce:group-focus-within:[transform:translateX(calc(var(--n)*48px))]" href="#" style="--n:2">
<img class="block w-full aspect-[3/4] object-cover rounded-lg" src="https://picsum.photos/seed/tw-fan-5/300/380" width="300" height="380" alt="Rippled desert dunes at midday">
</a>
</div>
<p class="m-0 font-stk05-mono text-[11px] tracking-[0.14em] uppercase text-stk05-muted">Hover to fan · Tab to spread</p>
</section>@import "tailwindcss";
@theme {
--color-stk05-paper: #e9e4db;
--color-stk05-paper: oklch(0.91 0.012 80);
--color-stk05-muted: #9a8f80;
--color-stk05-muted: oklch(0.65 0.02 80);
--color-stk05-focus: #6a7bf0;
--color-stk05-focus: oklch(0.60 0.17 265);
--font-stk05-sans: 'Archivo', system-ui, sans-serif;
--font-stk05-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-stk05: cubic-bezier(0.2, 0.85, 0.2, 1);
}@import "tailwindcss";
@theme {
--color-stk05-paper: #e9e4db;
--color-stk05-paper: oklch(0.91 0.012 80);
--color-stk05-muted: #9a8f80;
--color-stk05-muted: oklch(0.65 0.02 80);
--color-stk05-focus: #6a7bf0;
--color-stk05-focus: oklch(0.60 0.17 265);
--font-stk05-sans: 'Archivo', system-ui, sans-serif;
--font-stk05-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-stk05: cubic-bezier(0.2, 0.85, 0.2, 1);
}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-stk05-paper: #e9e4db;
--color-stk05-paper: oklch(0.91 0.012 80);
--color-stk05-muted: #9a8f80;
--color-stk05-muted: oklch(0.65 0.02 80);
--color-stk05-focus: #6a7bf0;
--color-stk05-focus: oklch(0.60 0.17 265);
--font-stk05-sans: 'Archivo', system-ui, sans-serif;
--font-stk05-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-stk05: cubic-bezier(0.2, 0.85, 0.2, 1);
}@import "tailwindcss";
@theme {
--color-stk05-paper: #e9e4db;
--color-stk05-paper: oklch(0.91 0.012 80);
--color-stk05-muted: #9a8f80;
--color-stk05-muted: oklch(0.65 0.02 80);
--color-stk05-focus: #6a7bf0;
--color-stk05-focus: oklch(0.60 0.17 265);
--font-stk05-sans: 'Archivo', system-ui, sans-serif;
--font-stk05-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-stk05: cubic-bezier(0.2, 0.85, 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&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&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
:nth-childrotation map disappears: one signed--nper card and a singlecalc()transform give the whole symmetrical fan. - The arc comes from a squared term in the same
calc()—translateY(calc(var(--n)*var(--n)*-2px))— so curvature and spread stay one number apart. - Five
transition-delayrules becomedelay-0/delay-[40ms]/delay-[80ms]typed on the cards in ramp order. transform-originhas no utility at this value, so[transform-origin:50%_130%]stays arbitrary — and it is the load-bearing declaration in the whole demo.- The per-card lift needs
hover:[transform:…]!: overriding agroup-hover:transform at equal specificity is exactly what the important modifier is for. - Reduced motion swaps choreography rather than duration —
motion-reduce:group-hover:[transform:translateX(…)]gives a straight spread with no rotation about an off-card pivot.
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 Fan Out Photo Stack on Hover
Source: https://codefronts.com/components/tailwind-stacked-cards/tailwind-fan-out-photo-stack-on-hover/
A tidy pile that fans into an arc, where the arithmetic replaces the rule. Every photo carries --n — its signed position from the centre — and one shared class list turns that into both the rotation and the arc lift: group-hover:[transform:rotate(calc(var(--n)*15deg))_translateY(calc(var(--n)*var(--n)*-2px))]. Five identical class lists, five different destinations, no :nth-child map anywhere.
## HTML
```html
<section class="stk-05 w-full min-h-svh flex flex-col items-center justify-center gap-[34px] px-5 py-15 font-stk05-sans bg-stk05-paper" aria-label="Fan-out photo stack">
<div class="group relative w-55 h-75">
<a class="absolute left-1/2 top-0 -ml-[90px] w-45 rounded-[14px] overflow-hidden bg-white p-2 pb-[22px] shadow-[0_16px_34px_-20px_rgba(60,40,20,0.6)] [transform-origin:50%_130%] transition-transform duration-500 ease-stk05 delay-0 group-hover:[transform:rotate(calc(var(--n)*15deg))_translateY(calc(var(--n)*var(--n)*-2px))] group-focus-within:[transform:rotate(calc(var(--n)*15deg))_translateY(calc(var(--n)*var(--n)*-2px))] hover:z-5 hover:[transform:rotate(calc(var(--n)*15deg))_translateY(-24px)]! focus-visible:z-5 focus-visible:outline-none focus-visible:shadow-[0_0_0_3px_var(--color-stk05-focus),0_20px_40px_-18px_rgba(60,40,20,0.7)] focus-visible:[transform:rotate(calc(var(--n)*15deg))_translateY(-24px)]! motion-reduce:duration-200 motion-reduce:group-hover:[transform:translateX(calc(var(--n)*48px))] motion-reduce:group-focus-within:[transform:translateX(calc(var(--n)*48px))]" href="#" style="--n:-2">
<img class="block w-full aspect-[3/4] object-cover rounded-lg" src="https://picsum.photos/seed/tw-fan-1/300/380" width="300" height="380" alt="Coastline seen from a cliff path">
</a>
<a class="absolute left-1/2 top-0 -ml-[90px] w-45 rounded-[14px] overflow-hidden bg-white p-2 pb-[22px] shadow-[0_16px_34px_-20px_rgba(60,40,20,0.6)] [transform-origin:50%_130%] transition-transform duration-500 ease-stk05 delay-[40ms] group-hover:[transform:rotate(calc(var(--n)*15deg))_translateY(calc(var(--n)*var(--n)*-2px))] group-focus-within:[transform:rotate(calc(var(--n)*15deg))_translateY(calc(var(--n)*var(--n)*-2px))] hover:z-5 hover:[transform:rotate(calc(var(--n)*15deg))_translateY(-24px)]! focus-visible:z-5 focus-visible:outline-none focus-visible:shadow-[0_0_0_3px_var(--color-stk05-focus),0_20px_40px_-18px_rgba(60,40,20,0.7)] focus-visible:[transform:rotate(calc(var(--n)*15deg))_translateY(-24px)]! motion-reduce:duration-200 motion-reduce:group-hover:[transform:translateX(calc(var(--n)*48px))] motion-reduce:group-focus-within:[transform:translateX(calc(var(--n)*48px))]" href="#" style="--n:-1">
<img class="block w-full aspect-[3/4] object-cover rounded-lg" src="https://picsum.photos/seed/tw-fan-2/300/380" width="300" height="380" alt="Narrow street in an old town">
</a>
<a class="absolute left-1/2 top-0 -ml-[90px] w-45 rounded-[14px] overflow-hidden bg-white p-2 pb-[22px] shadow-[0_16px_34px_-20px_rgba(60,40,20,0.6)] [transform-origin:50%_130%] transition-transform duration-500 ease-stk05 delay-[80ms] group-hover:[transform:rotate(calc(var(--n)*15deg))_translateY(calc(var(--n)*var(--n)*-2px))] group-focus-within:[transform:rotate(calc(var(--n)*15deg))_translateY(calc(var(--n)*var(--n)*-2px))] hover:z-5 hover:[transform:rotate(calc(var(--n)*15deg))_translateY(-24px)]! focus-visible:z-5 focus-visible:outline-none focus-visible:shadow-[0_0_0_3px_var(--color-stk05-focus),0_20px_40px_-18px_rgba(60,40,20,0.7)] focus-visible:[transform:rotate(calc(var(--n)*15deg))_translateY(-24px)]! motion-reduce:duration-200 motion-reduce:group-hover:[transform:translateX(calc(var(--n)*48px))] motion-reduce:group-focus-within:[transform:translateX(calc(var(--n)*48px))]" href="#" style="--n:0">
<img class="block w-full aspect-[3/4] object-cover rounded-lg" src="https://picsum.photos/seed/tw-fan-3/300/380" width="300" height="380" alt="Stone hut on a mountain ridge">
</a>
<a class="absolute left-1/2 top-0 -ml-[90px] w-45 rounded-[14px] overflow-hidden bg-white p-2 pb-[22px] shadow-[0_16px_34px_-20px_rgba(60,40,20,0.6)] [transform-origin:50%_130%] transition-transform duration-500 ease-stk05 delay-[40ms] group-hover:[transform:rotate(calc(var(--n)*15deg))_translateY(calc(var(--n)*var(--n)*-2px))] group-focus-within:[transform:rotate(calc(var(--n)*15deg))_translateY(calc(var(--n)*var(--n)*-2px))] hover:z-5 hover:[transform:rotate(calc(var(--n)*15deg))_translateY(-24px)]! focus-visible:z-5 focus-visible:outline-none focus-visible:shadow-[0_0_0_3px_var(--color-stk05-focus),0_20px_40px_-18px_rgba(60,40,20,0.7)] focus-visible:[transform:rotate(calc(var(--n)*15deg))_translateY(-24px)]! motion-reduce:duration-200 motion-reduce:group-hover:[transform:translateX(calc(var(--n)*48px))] motion-reduce:group-focus-within:[transform:translateX(calc(var(--n)*48px))]" href="#" style="--n:1">
<img class="block w-full aspect-[3/4] object-cover rounded-lg" src="https://picsum.photos/seed/tw-fan-4/300/380" width="300" height="380" alt="Harbour at dusk with moored boats">
</a>
<a class="absolute left-1/2 top-0 -ml-[90px] w-45 rounded-[14px] overflow-hidden bg-white p-2 pb-[22px] shadow-[0_16px_34px_-20px_rgba(60,40,20,0.6)] [transform-origin:50%_130%] transition-transform duration-500 ease-stk05 delay-0 group-hover:[transform:rotate(calc(var(--n)*15deg))_translateY(calc(var(--n)*var(--n)*-2px))] group-focus-within:[transform:rotate(calc(var(--n)*15deg))_translateY(calc(var(--n)*var(--n)*-2px))] hover:z-5 hover:[transform:rotate(calc(var(--n)*15deg))_translateY(-24px)]! focus-visible:z-5 focus-visible:outline-none focus-visible:shadow-[0_0_0_3px_var(--color-stk05-focus),0_20px_40px_-18px_rgba(60,40,20,0.7)] focus-visible:[transform:rotate(calc(var(--n)*15deg))_translateY(-24px)]! motion-reduce:duration-200 motion-reduce:group-hover:[transform:translateX(calc(var(--n)*48px))] motion-reduce:group-focus-within:[transform:translateX(calc(var(--n)*48px))]" href="#" style="--n:2">
<img class="block w-full aspect-[3/4] object-cover rounded-lg" src="https://picsum.photos/seed/tw-fan-5/300/380" width="300" height="380" alt="Rippled desert dunes at midday">
</a>
</div>
<p class="m-0 font-stk05-mono text-[11px] tracking-[0.14em] uppercase text-stk05-muted">Hover to fan · Tab to spread</p>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-stk05-paper: #e9e4db;
--color-stk05-paper: oklch(0.91 0.012 80);
--color-stk05-muted: #9a8f80;
--color-stk05-muted: oklch(0.65 0.02 80);
--color-stk05-focus: #6a7bf0;
--color-stk05-focus: oklch(0.60 0.17 265);
--font-stk05-sans: 'Archivo', system-ui, sans-serif;
--font-stk05-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-stk05: cubic-bezier(0.2, 0.85, 0.2, 1);
}
```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 Fan Out Photo Stack on Hover
Source: https://codefronts.com/components/tailwind-stacked-cards/tailwind-fan-out-photo-stack-on-hover/
A tidy pile that fans into an arc, where the arithmetic replaces the rule. Every photo carries --n — its signed position from the centre — and one shared class list turns that into both the rotation and the arc lift: group-hover:[transform:rotate(calc(var(--n)*15deg))_translateY(calc(var(--n)*var(--n)*-2px))]. Five identical class lists, five different destinations, no :nth-child map anywhere.
## HTML
```html
<section class="stk-05 w-full min-h-svh flex flex-col items-center justify-center gap-[34px] px-5 py-15 font-stk05-sans bg-stk05-paper" aria-label="Fan-out photo stack">
<div class="group relative w-55 h-75">
<a class="absolute left-1/2 top-0 -ml-[90px] w-45 rounded-[14px] overflow-hidden bg-white p-2 pb-[22px] shadow-[0_16px_34px_-20px_rgba(60,40,20,0.6)] [transform-origin:50%_130%] transition-transform duration-500 ease-stk05 delay-0 group-hover:[transform:rotate(calc(var(--n)*15deg))_translateY(calc(var(--n)*var(--n)*-2px))] group-focus-within:[transform:rotate(calc(var(--n)*15deg))_translateY(calc(var(--n)*var(--n)*-2px))] hover:z-5 hover:[transform:rotate(calc(var(--n)*15deg))_translateY(-24px)]! focus-visible:z-5 focus-visible:outline-none focus-visible:shadow-[0_0_0_3px_var(--color-stk05-focus),0_20px_40px_-18px_rgba(60,40,20,0.7)] focus-visible:[transform:rotate(calc(var(--n)*15deg))_translateY(-24px)]! motion-reduce:duration-200 motion-reduce:group-hover:[transform:translateX(calc(var(--n)*48px))] motion-reduce:group-focus-within:[transform:translateX(calc(var(--n)*48px))]" href="#" style="--n:-2">
<img class="block w-full aspect-[3/4] object-cover rounded-lg" src="https://picsum.photos/seed/tw-fan-1/300/380" width="300" height="380" alt="Coastline seen from a cliff path">
</a>
<a class="absolute left-1/2 top-0 -ml-[90px] w-45 rounded-[14px] overflow-hidden bg-white p-2 pb-[22px] shadow-[0_16px_34px_-20px_rgba(60,40,20,0.6)] [transform-origin:50%_130%] transition-transform duration-500 ease-stk05 delay-[40ms] group-hover:[transform:rotate(calc(var(--n)*15deg))_translateY(calc(var(--n)*var(--n)*-2px))] group-focus-within:[transform:rotate(calc(var(--n)*15deg))_translateY(calc(var(--n)*var(--n)*-2px))] hover:z-5 hover:[transform:rotate(calc(var(--n)*15deg))_translateY(-24px)]! focus-visible:z-5 focus-visible:outline-none focus-visible:shadow-[0_0_0_3px_var(--color-stk05-focus),0_20px_40px_-18px_rgba(60,40,20,0.7)] focus-visible:[transform:rotate(calc(var(--n)*15deg))_translateY(-24px)]! motion-reduce:duration-200 motion-reduce:group-hover:[transform:translateX(calc(var(--n)*48px))] motion-reduce:group-focus-within:[transform:translateX(calc(var(--n)*48px))]" href="#" style="--n:-1">
<img class="block w-full aspect-[3/4] object-cover rounded-lg" src="https://picsum.photos/seed/tw-fan-2/300/380" width="300" height="380" alt="Narrow street in an old town">
</a>
<a class="absolute left-1/2 top-0 -ml-[90px] w-45 rounded-[14px] overflow-hidden bg-white p-2 pb-[22px] shadow-[0_16px_34px_-20px_rgba(60,40,20,0.6)] [transform-origin:50%_130%] transition-transform duration-500 ease-stk05 delay-[80ms] group-hover:[transform:rotate(calc(var(--n)*15deg))_translateY(calc(var(--n)*var(--n)*-2px))] group-focus-within:[transform:rotate(calc(var(--n)*15deg))_translateY(calc(var(--n)*var(--n)*-2px))] hover:z-5 hover:[transform:rotate(calc(var(--n)*15deg))_translateY(-24px)]! focus-visible:z-5 focus-visible:outline-none focus-visible:shadow-[0_0_0_3px_var(--color-stk05-focus),0_20px_40px_-18px_rgba(60,40,20,0.7)] focus-visible:[transform:rotate(calc(var(--n)*15deg))_translateY(-24px)]! motion-reduce:duration-200 motion-reduce:group-hover:[transform:translateX(calc(var(--n)*48px))] motion-reduce:group-focus-within:[transform:translateX(calc(var(--n)*48px))]" href="#" style="--n:0">
<img class="block w-full aspect-[3/4] object-cover rounded-lg" src="https://picsum.photos/seed/tw-fan-3/300/380" width="300" height="380" alt="Stone hut on a mountain ridge">
</a>
<a class="absolute left-1/2 top-0 -ml-[90px] w-45 rounded-[14px] overflow-hidden bg-white p-2 pb-[22px] shadow-[0_16px_34px_-20px_rgba(60,40,20,0.6)] [transform-origin:50%_130%] transition-transform duration-500 ease-stk05 delay-[40ms] group-hover:[transform:rotate(calc(var(--n)*15deg))_translateY(calc(var(--n)*var(--n)*-2px))] group-focus-within:[transform:rotate(calc(var(--n)*15deg))_translateY(calc(var(--n)*var(--n)*-2px))] hover:z-5 hover:[transform:rotate(calc(var(--n)*15deg))_translateY(-24px)]! focus-visible:z-5 focus-visible:outline-none focus-visible:shadow-[0_0_0_3px_var(--color-stk05-focus),0_20px_40px_-18px_rgba(60,40,20,0.7)] focus-visible:[transform:rotate(calc(var(--n)*15deg))_translateY(-24px)]! motion-reduce:duration-200 motion-reduce:group-hover:[transform:translateX(calc(var(--n)*48px))] motion-reduce:group-focus-within:[transform:translateX(calc(var(--n)*48px))]" href="#" style="--n:1">
<img class="block w-full aspect-[3/4] object-cover rounded-lg" src="https://picsum.photos/seed/tw-fan-4/300/380" width="300" height="380" alt="Harbour at dusk with moored boats">
</a>
<a class="absolute left-1/2 top-0 -ml-[90px] w-45 rounded-[14px] overflow-hidden bg-white p-2 pb-[22px] shadow-[0_16px_34px_-20px_rgba(60,40,20,0.6)] [transform-origin:50%_130%] transition-transform duration-500 ease-stk05 delay-0 group-hover:[transform:rotate(calc(var(--n)*15deg))_translateY(calc(var(--n)*var(--n)*-2px))] group-focus-within:[transform:rotate(calc(var(--n)*15deg))_translateY(calc(var(--n)*var(--n)*-2px))] hover:z-5 hover:[transform:rotate(calc(var(--n)*15deg))_translateY(-24px)]! focus-visible:z-5 focus-visible:outline-none focus-visible:shadow-[0_0_0_3px_var(--color-stk05-focus),0_20px_40px_-18px_rgba(60,40,20,0.7)] focus-visible:[transform:rotate(calc(var(--n)*15deg))_translateY(-24px)]! motion-reduce:duration-200 motion-reduce:group-hover:[transform:translateX(calc(var(--n)*48px))] motion-reduce:group-focus-within:[transform:translateX(calc(var(--n)*48px))]" href="#" style="--n:2">
<img class="block w-full aspect-[3/4] object-cover rounded-lg" src="https://picsum.photos/seed/tw-fan-5/300/380" width="300" height="380" alt="Rippled desert dunes at midday">
</a>
</div>
<p class="m-0 font-stk05-mono text-[11px] tracking-[0.14em] uppercase text-stk05-muted">Hover to fan · Tab to spread</p>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-stk05-paper: #e9e4db;
--color-stk05-paper: oklch(0.91 0.012 80);
--color-stk05-muted: #9a8f80;
--color-stk05-muted: oklch(0.65 0.02 80);
--color-stk05-focus: #6a7bf0;
--color-stk05-focus: oklch(0.60 0.17 265);
--font-stk05-sans: 'Archivo', system-ui, sans-serif;
--font-stk05-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-stk05: cubic-bezier(0.2, 0.85, 0.2, 1);
}
```How this works
The index does the work a rule would. Each photo declares where it sits relative to the middle:
<a style="--n:-2" class="… group-hover:[transform:rotate(calc(var(--n)*15deg))
_translateY(calc(var(--n)*var(--n)*-2px))]">The linear term spreads the fan; the squared term lifts the outer cards less than the inner ones, which is what makes the top edge read as an arc instead of a straight line. Both fall out of one number, and adding a sixth photo means typing --n:3 — nothing else.
The pivot is the whole trick and it has no utility. transform-origin ships a named scale (origin-bottom, origin-top-left) but not the below-the-card pivot a hand of cards needs, so [transform-origin:50%_130%] is the idiomatic answer. Move it to 50%_100% for a tighter, held-in-the-hand feel.
Individual hover has to shout. Lifting one card while the fan is open means overriding a transform the group already set, at equal specificity. hover:[transform:…]! with the important modifier is the honest fix; the alternative is a deeper selector, which utilities cannot express.
Reduced motion changes the choreography, not the speed. Rotation around an off-card pivot is exactly the kind of motion that triggers vestibular symptoms, so under motion-reduce: the fan becomes a straight horizontal spread — still informative, no arc.
Make it yours
- Widen or tighten the arc by changing the
15degmultiplier inside thegroup-hover:transform. - Flatten or exaggerate the curve with the squared term's
-2px; set it to0for a flat spread. - Move the pivot with
[transform-origin:50%_130%]— lower values pivot closer to the card and spread wider. - Retime the cascade by editing the per-card
delay-[…]utilities; they are typed on the elements in ramp order. - Add a photo by copying one
<a>and giving it the next--n.
Gotchas — read before shipping
- The pivot must be shared. Without
[transform-origin:50%_130%]each card rotates about its own centre and the pile scatters instead of fanning. - Give the stage room. The fan is wider than the pile, and the parent needs the padding or the outer cards clip.
group-focus-within:is required, not decorative. Tabbing through five links with onlygroup-hover:defined leaves the keyboard user staring at an unchanged pile.- The per-card
hover:[transform:…]needs the!modifier to beatgroup-hover:— same specificity, and source order is not something you control in a utility sheet. - Arbitrary
calc()cannot contain spaces:rotate(calc(var(--n)*15deg)), and the underscore escape between the two transform functions. - Keep
will-change-transformoff the resting state unless you measured a benefit; on five layered images it costs memory for nothing.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
Transforms, calc() in transforms and :focus-within are universal (Chrome 49+, Safari 10+, Firefox 52+). The floor is Tailwind v4's own generated sheet.