19 / 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 Scattered Photos Pinch to Centre on Hover
Hover one photo and the whole cluster gathers toward the centre and dims, while the one you touched brightens and rises. The group behaviour is group-has-hover: — Tailwind's variant form of :has(:hover) — and the spotlight override needs the ! modifier, because the hovered photo has to beat a rule its own parent just applied to it.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="stk-19 w-full min-h-svh flex items-center justify-center px-5 py-15 font-stk19-sans bg-stk19-bg" aria-label="Pinch-to-focus photo cluster">
<div class="group relative w-[min(360px,90vw)] h-85">
<a class="absolute left-1/2 top-1/2 -ml-[75px] -mt-[75px] size-37.5 overflow-hidden rounded-[14px] shadow-[0_16px_34px_-18px_rgba(0,0,0,0.8)] [transform:translate(var(--x),var(--y))_rotate(var(--r))] transition-[transform,filter] duration-450 ease-stk19 group-has-hover:brightness-50 group-has-hover:[transform:translate(calc(var(--x)*0.3),calc(var(--y)*0.3))_rotate(0deg)] group-has-focus-visible:brightness-50 group-has-focus-visible:[transform:translate(calc(var(--x)*0.3),calc(var(--y)*0.3))_rotate(0deg)] hover:z-10 hover:brightness-110! hover:[transform:translate(0,0)_rotate(0deg)_scale(1.25)]! focus-visible:z-10 focus-visible:outline-none focus-visible:brightness-110! focus-visible:[transform:translate(0,0)_rotate(0deg)_scale(1.25)]! focus-visible:shadow-[0_0_0_3px_var(--color-stk19-ring),0_24px_44px_-18px_rgba(0,0,0,0.9)] motion-reduce:group-has-hover:[transform:translate(var(--x),var(--y))_rotate(var(--r))] motion-reduce:hover:[transform:translate(var(--x),var(--y))_rotate(var(--r))_scale(1.1)]!" href="#" style="--x:-120px;--y:-40px;--r:-7deg">
<img class="block size-full object-cover" src="https://picsum.photos/seed/tw-pinch-1/260/260" width="260" height="260" alt="Street market stall at night">
</a>
<a class="absolute left-1/2 top-1/2 -ml-[75px] -mt-[75px] size-37.5 overflow-hidden rounded-[14px] shadow-[0_16px_34px_-18px_rgba(0,0,0,0.8)] [transform:translate(var(--x),var(--y))_rotate(var(--r))] transition-[transform,filter] duration-450 ease-stk19 group-has-hover:brightness-50 group-has-hover:[transform:translate(calc(var(--x)*0.3),calc(var(--y)*0.3))_rotate(0deg)] group-has-focus-visible:brightness-50 group-has-focus-visible:[transform:translate(calc(var(--x)*0.3),calc(var(--y)*0.3))_rotate(0deg)] hover:z-10 hover:brightness-110! hover:[transform:translate(0,0)_rotate(0deg)_scale(1.25)]! focus-visible:z-10 focus-visible:outline-none focus-visible:brightness-110! focus-visible:[transform:translate(0,0)_rotate(0deg)_scale(1.25)]! focus-visible:shadow-[0_0_0_3px_var(--color-stk19-ring),0_24px_44px_-18px_rgba(0,0,0,0.9)] motion-reduce:group-has-hover:[transform:translate(var(--x),var(--y))_rotate(var(--r))] motion-reduce:hover:[transform:translate(var(--x),var(--y))_rotate(var(--r))_scale(1.1)]!" href="#" style="--x:110px;--y:-60px;--r:6deg">
<img class="block size-full object-cover" src="https://picsum.photos/seed/tw-pinch-2/260/260" width="260" height="260" alt="Neon-lit alleyway">
</a>
<a class="absolute left-1/2 top-1/2 -ml-[75px] -mt-[75px] size-37.5 overflow-hidden rounded-[14px] shadow-[0_16px_34px_-18px_rgba(0,0,0,0.8)] [transform:translate(var(--x),var(--y))_rotate(var(--r))] transition-[transform,filter] duration-450 ease-stk19 group-has-hover:brightness-50 group-has-hover:[transform:translate(calc(var(--x)*0.3),calc(var(--y)*0.3))_rotate(0deg)] group-has-focus-visible:brightness-50 group-has-focus-visible:[transform:translate(calc(var(--x)*0.3),calc(var(--y)*0.3))_rotate(0deg)] hover:z-10 hover:brightness-110! hover:[transform:translate(0,0)_rotate(0deg)_scale(1.25)]! focus-visible:z-10 focus-visible:outline-none focus-visible:brightness-110! focus-visible:[transform:translate(0,0)_rotate(0deg)_scale(1.25)]! focus-visible:shadow-[0_0_0_3px_var(--color-stk19-ring),0_24px_44px_-18px_rgba(0,0,0,0.9)] motion-reduce:group-has-hover:[transform:translate(var(--x),var(--y))_rotate(var(--r))] motion-reduce:hover:[transform:translate(var(--x),var(--y))_rotate(var(--r))_scale(1.1)]!" href="#" style="--x:-90px;--y:70px;--r:5deg">
<img class="block size-full object-cover" src="https://picsum.photos/seed/tw-pinch-3/260/260" width="260" height="260" alt="Rooftop garden in summer">
</a>
<a class="absolute left-1/2 top-1/2 -ml-[75px] -mt-[75px] size-37.5 overflow-hidden rounded-[14px] shadow-[0_16px_34px_-18px_rgba(0,0,0,0.8)] [transform:translate(var(--x),var(--y))_rotate(var(--r))] transition-[transform,filter] duration-450 ease-stk19 group-has-hover:brightness-50 group-has-hover:[transform:translate(calc(var(--x)*0.3),calc(var(--y)*0.3))_rotate(0deg)] group-has-focus-visible:brightness-50 group-has-focus-visible:[transform:translate(calc(var(--x)*0.3),calc(var(--y)*0.3))_rotate(0deg)] hover:z-10 hover:brightness-110! hover:[transform:translate(0,0)_rotate(0deg)_scale(1.25)]! focus-visible:z-10 focus-visible:outline-none focus-visible:brightness-110! focus-visible:[transform:translate(0,0)_rotate(0deg)_scale(1.25)]! focus-visible:shadow-[0_0_0_3px_var(--color-stk19-ring),0_24px_44px_-18px_rgba(0,0,0,0.9)] motion-reduce:group-has-hover:[transform:translate(var(--x),var(--y))_rotate(var(--r))] motion-reduce:hover:[transform:translate(var(--x),var(--y))_rotate(var(--r))_scale(1.1)]!" href="#" style="--x:120px;--y:60px;--r:-6deg">
<img class="block size-full object-cover" src="https://picsum.photos/seed/tw-pinch-4/260/260" width="260" height="260" alt="Harbour lights reflected on water">
</a>
<a class="absolute left-1/2 top-1/2 -ml-[75px] -mt-[75px] size-37.5 overflow-hidden rounded-[14px] shadow-[0_16px_34px_-18px_rgba(0,0,0,0.8)] [transform:translate(var(--x),var(--y))_rotate(var(--r))] transition-[transform,filter] duration-450 ease-stk19 group-has-hover:brightness-50 group-has-hover:[transform:translate(calc(var(--x)*0.3),calc(var(--y)*0.3))_rotate(0deg)] group-has-focus-visible:brightness-50 group-has-focus-visible:[transform:translate(calc(var(--x)*0.3),calc(var(--y)*0.3))_rotate(0deg)] hover:z-10 hover:brightness-110! hover:[transform:translate(0,0)_rotate(0deg)_scale(1.25)]! focus-visible:z-10 focus-visible:outline-none focus-visible:brightness-110! focus-visible:[transform:translate(0,0)_rotate(0deg)_scale(1.25)]! focus-visible:shadow-[0_0_0_3px_var(--color-stk19-ring),0_24px_44px_-18px_rgba(0,0,0,0.9)] motion-reduce:group-has-hover:[transform:translate(var(--x),var(--y))_rotate(var(--r))] motion-reduce:hover:[transform:translate(var(--x),var(--y))_rotate(var(--r))_scale(1.1)]!" href="#" style="--x:0px;--y:0px;--r:2deg">
<img class="block size-full object-cover" src="https://picsum.photos/seed/tw-pinch-5/260/260" width="260" height="260" alt="Landscape blurred past a train window">
</a>
</div>
</section><section class="stk-19 w-full min-h-svh flex items-center justify-center px-5 py-15 font-stk19-sans bg-stk19-bg" aria-label="Pinch-to-focus photo cluster">
<div class="group relative w-[min(360px,90vw)] h-85">
<a class="absolute left-1/2 top-1/2 -ml-[75px] -mt-[75px] size-37.5 overflow-hidden rounded-[14px] shadow-[0_16px_34px_-18px_rgba(0,0,0,0.8)] [transform:translate(var(--x),var(--y))_rotate(var(--r))] transition-[transform,filter] duration-450 ease-stk19 group-has-hover:brightness-50 group-has-hover:[transform:translate(calc(var(--x)*0.3),calc(var(--y)*0.3))_rotate(0deg)] group-has-focus-visible:brightness-50 group-has-focus-visible:[transform:translate(calc(var(--x)*0.3),calc(var(--y)*0.3))_rotate(0deg)] hover:z-10 hover:brightness-110! hover:[transform:translate(0,0)_rotate(0deg)_scale(1.25)]! focus-visible:z-10 focus-visible:outline-none focus-visible:brightness-110! focus-visible:[transform:translate(0,0)_rotate(0deg)_scale(1.25)]! focus-visible:shadow-[0_0_0_3px_var(--color-stk19-ring),0_24px_44px_-18px_rgba(0,0,0,0.9)] motion-reduce:group-has-hover:[transform:translate(var(--x),var(--y))_rotate(var(--r))] motion-reduce:hover:[transform:translate(var(--x),var(--y))_rotate(var(--r))_scale(1.1)]!" href="#" style="--x:-120px;--y:-40px;--r:-7deg">
<img class="block size-full object-cover" src="https://picsum.photos/seed/tw-pinch-1/260/260" width="260" height="260" alt="Street market stall at night">
</a>
<a class="absolute left-1/2 top-1/2 -ml-[75px] -mt-[75px] size-37.5 overflow-hidden rounded-[14px] shadow-[0_16px_34px_-18px_rgba(0,0,0,0.8)] [transform:translate(var(--x),var(--y))_rotate(var(--r))] transition-[transform,filter] duration-450 ease-stk19 group-has-hover:brightness-50 group-has-hover:[transform:translate(calc(var(--x)*0.3),calc(var(--y)*0.3))_rotate(0deg)] group-has-focus-visible:brightness-50 group-has-focus-visible:[transform:translate(calc(var(--x)*0.3),calc(var(--y)*0.3))_rotate(0deg)] hover:z-10 hover:brightness-110! hover:[transform:translate(0,0)_rotate(0deg)_scale(1.25)]! focus-visible:z-10 focus-visible:outline-none focus-visible:brightness-110! focus-visible:[transform:translate(0,0)_rotate(0deg)_scale(1.25)]! focus-visible:shadow-[0_0_0_3px_var(--color-stk19-ring),0_24px_44px_-18px_rgba(0,0,0,0.9)] motion-reduce:group-has-hover:[transform:translate(var(--x),var(--y))_rotate(var(--r))] motion-reduce:hover:[transform:translate(var(--x),var(--y))_rotate(var(--r))_scale(1.1)]!" href="#" style="--x:110px;--y:-60px;--r:6deg">
<img class="block size-full object-cover" src="https://picsum.photos/seed/tw-pinch-2/260/260" width="260" height="260" alt="Neon-lit alleyway">
</a>
<a class="absolute left-1/2 top-1/2 -ml-[75px] -mt-[75px] size-37.5 overflow-hidden rounded-[14px] shadow-[0_16px_34px_-18px_rgba(0,0,0,0.8)] [transform:translate(var(--x),var(--y))_rotate(var(--r))] transition-[transform,filter] duration-450 ease-stk19 group-has-hover:brightness-50 group-has-hover:[transform:translate(calc(var(--x)*0.3),calc(var(--y)*0.3))_rotate(0deg)] group-has-focus-visible:brightness-50 group-has-focus-visible:[transform:translate(calc(var(--x)*0.3),calc(var(--y)*0.3))_rotate(0deg)] hover:z-10 hover:brightness-110! hover:[transform:translate(0,0)_rotate(0deg)_scale(1.25)]! focus-visible:z-10 focus-visible:outline-none focus-visible:brightness-110! focus-visible:[transform:translate(0,0)_rotate(0deg)_scale(1.25)]! focus-visible:shadow-[0_0_0_3px_var(--color-stk19-ring),0_24px_44px_-18px_rgba(0,0,0,0.9)] motion-reduce:group-has-hover:[transform:translate(var(--x),var(--y))_rotate(var(--r))] motion-reduce:hover:[transform:translate(var(--x),var(--y))_rotate(var(--r))_scale(1.1)]!" href="#" style="--x:-90px;--y:70px;--r:5deg">
<img class="block size-full object-cover" src="https://picsum.photos/seed/tw-pinch-3/260/260" width="260" height="260" alt="Rooftop garden in summer">
</a>
<a class="absolute left-1/2 top-1/2 -ml-[75px] -mt-[75px] size-37.5 overflow-hidden rounded-[14px] shadow-[0_16px_34px_-18px_rgba(0,0,0,0.8)] [transform:translate(var(--x),var(--y))_rotate(var(--r))] transition-[transform,filter] duration-450 ease-stk19 group-has-hover:brightness-50 group-has-hover:[transform:translate(calc(var(--x)*0.3),calc(var(--y)*0.3))_rotate(0deg)] group-has-focus-visible:brightness-50 group-has-focus-visible:[transform:translate(calc(var(--x)*0.3),calc(var(--y)*0.3))_rotate(0deg)] hover:z-10 hover:brightness-110! hover:[transform:translate(0,0)_rotate(0deg)_scale(1.25)]! focus-visible:z-10 focus-visible:outline-none focus-visible:brightness-110! focus-visible:[transform:translate(0,0)_rotate(0deg)_scale(1.25)]! focus-visible:shadow-[0_0_0_3px_var(--color-stk19-ring),0_24px_44px_-18px_rgba(0,0,0,0.9)] motion-reduce:group-has-hover:[transform:translate(var(--x),var(--y))_rotate(var(--r))] motion-reduce:hover:[transform:translate(var(--x),var(--y))_rotate(var(--r))_scale(1.1)]!" href="#" style="--x:120px;--y:60px;--r:-6deg">
<img class="block size-full object-cover" src="https://picsum.photos/seed/tw-pinch-4/260/260" width="260" height="260" alt="Harbour lights reflected on water">
</a>
<a class="absolute left-1/2 top-1/2 -ml-[75px] -mt-[75px] size-37.5 overflow-hidden rounded-[14px] shadow-[0_16px_34px_-18px_rgba(0,0,0,0.8)] [transform:translate(var(--x),var(--y))_rotate(var(--r))] transition-[transform,filter] duration-450 ease-stk19 group-has-hover:brightness-50 group-has-hover:[transform:translate(calc(var(--x)*0.3),calc(var(--y)*0.3))_rotate(0deg)] group-has-focus-visible:brightness-50 group-has-focus-visible:[transform:translate(calc(var(--x)*0.3),calc(var(--y)*0.3))_rotate(0deg)] hover:z-10 hover:brightness-110! hover:[transform:translate(0,0)_rotate(0deg)_scale(1.25)]! focus-visible:z-10 focus-visible:outline-none focus-visible:brightness-110! focus-visible:[transform:translate(0,0)_rotate(0deg)_scale(1.25)]! focus-visible:shadow-[0_0_0_3px_var(--color-stk19-ring),0_24px_44px_-18px_rgba(0,0,0,0.9)] motion-reduce:group-has-hover:[transform:translate(var(--x),var(--y))_rotate(var(--r))] motion-reduce:hover:[transform:translate(var(--x),var(--y))_rotate(var(--r))_scale(1.1)]!" href="#" style="--x:0px;--y:0px;--r:2deg">
<img class="block size-full object-cover" src="https://picsum.photos/seed/tw-pinch-5/260/260" width="260" height="260" alt="Landscape blurred past a train window">
</a>
</div>
</section>@import "tailwindcss";
@theme {
--color-stk19-bg: #0c0d13;
--color-stk19-bg: oklch(0.14 0.012 275);
--color-stk19-ring: #5aa8e8;
--color-stk19-ring: oklch(0.70 0.15 250);
--font-stk19-sans: 'Archivo', system-ui, sans-serif;
--ease-stk19: cubic-bezier(0.3, 0.9, 0.3, 1);
}@import "tailwindcss";
@theme {
--color-stk19-bg: #0c0d13;
--color-stk19-bg: oklch(0.14 0.012 275);
--color-stk19-ring: #5aa8e8;
--color-stk19-ring: oklch(0.70 0.15 250);
--font-stk19-sans: 'Archivo', system-ui, sans-serif;
--ease-stk19: cubic-bezier(0.3, 0.9, 0.3, 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-stk19-bg: #0c0d13;
--color-stk19-bg: oklch(0.14 0.012 275);
--color-stk19-ring: #5aa8e8;
--color-stk19-ring: oklch(0.70 0.15 250);
--font-stk19-sans: 'Archivo', system-ui, sans-serif;
--ease-stk19: cubic-bezier(0.3, 0.9, 0.3, 1);
}@import "tailwindcss";
@theme {
--color-stk19-bg: #0c0d13;
--color-stk19-bg: oklch(0.14 0.012 275);
--color-stk19-ring: #5aa8e8;
--color-stk19-ring: oklch(0.70 0.15 250);
--font-stk19-sans: 'Archivo', system-ui, sans-serif;
--ease-stk19: 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&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&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. :has(:hover)becomesgroup-has-hover:— written from the child, which is the opposite direction from the CSS and reads better next to the state it modifies.- The pinch multiplies each photo's own scatter by
0.3rather than moving to fixed coordinates, so every card converges along its own vector and the gather looks magnetic. - The spotlight needs the
!modifier: the hovered photo is also matched bygroup-has-hover:at equal specificity, and utilities offer no way to out-specify it. - That is the second and last legitimate
!in the collection (the other is demo 02) — both are a narrow state overriding a broad one. - Where
:has()is unsupported the group pinch silently does not apply and each photo still lifts on its own hover, because the individual state is an independent variant. - Reduced motion freezes the scatter and keeps the brightness change: a filter is not vestibular, a five-element convergence is.
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 Scattered Photos Pinch to Centre on Hover
Source: https://codefronts.com/components/tailwind-stacked-cards/tailwind-scattered-photos-pinch-to-centre-on-hover/
Hover one photo and the whole cluster gathers toward the centre and dims, while the one you touched brightens and rises. The group behaviour is group-has-hover: — Tailwind's variant form of :has(:hover) — and the spotlight override needs the ! modifier, because the hovered photo has to beat a rule its own parent just applied to it.
## HTML
```html
<section class="stk-19 w-full min-h-svh flex items-center justify-center px-5 py-15 font-stk19-sans bg-stk19-bg" aria-label="Pinch-to-focus photo cluster">
<div class="group relative w-[min(360px,90vw)] h-85">
<a class="absolute left-1/2 top-1/2 -ml-[75px] -mt-[75px] size-37.5 overflow-hidden rounded-[14px] shadow-[0_16px_34px_-18px_rgba(0,0,0,0.8)] [transform:translate(var(--x),var(--y))_rotate(var(--r))] transition-[transform,filter] duration-450 ease-stk19 group-has-hover:brightness-50 group-has-hover:[transform:translate(calc(var(--x)*0.3),calc(var(--y)*0.3))_rotate(0deg)] group-has-focus-visible:brightness-50 group-has-focus-visible:[transform:translate(calc(var(--x)*0.3),calc(var(--y)*0.3))_rotate(0deg)] hover:z-10 hover:brightness-110! hover:[transform:translate(0,0)_rotate(0deg)_scale(1.25)]! focus-visible:z-10 focus-visible:outline-none focus-visible:brightness-110! focus-visible:[transform:translate(0,0)_rotate(0deg)_scale(1.25)]! focus-visible:shadow-[0_0_0_3px_var(--color-stk19-ring),0_24px_44px_-18px_rgba(0,0,0,0.9)] motion-reduce:group-has-hover:[transform:translate(var(--x),var(--y))_rotate(var(--r))] motion-reduce:hover:[transform:translate(var(--x),var(--y))_rotate(var(--r))_scale(1.1)]!" href="#" style="--x:-120px;--y:-40px;--r:-7deg">
<img class="block size-full object-cover" src="https://picsum.photos/seed/tw-pinch-1/260/260" width="260" height="260" alt="Street market stall at night">
</a>
<a class="absolute left-1/2 top-1/2 -ml-[75px] -mt-[75px] size-37.5 overflow-hidden rounded-[14px] shadow-[0_16px_34px_-18px_rgba(0,0,0,0.8)] [transform:translate(var(--x),var(--y))_rotate(var(--r))] transition-[transform,filter] duration-450 ease-stk19 group-has-hover:brightness-50 group-has-hover:[transform:translate(calc(var(--x)*0.3),calc(var(--y)*0.3))_rotate(0deg)] group-has-focus-visible:brightness-50 group-has-focus-visible:[transform:translate(calc(var(--x)*0.3),calc(var(--y)*0.3))_rotate(0deg)] hover:z-10 hover:brightness-110! hover:[transform:translate(0,0)_rotate(0deg)_scale(1.25)]! focus-visible:z-10 focus-visible:outline-none focus-visible:brightness-110! focus-visible:[transform:translate(0,0)_rotate(0deg)_scale(1.25)]! focus-visible:shadow-[0_0_0_3px_var(--color-stk19-ring),0_24px_44px_-18px_rgba(0,0,0,0.9)] motion-reduce:group-has-hover:[transform:translate(var(--x),var(--y))_rotate(var(--r))] motion-reduce:hover:[transform:translate(var(--x),var(--y))_rotate(var(--r))_scale(1.1)]!" href="#" style="--x:110px;--y:-60px;--r:6deg">
<img class="block size-full object-cover" src="https://picsum.photos/seed/tw-pinch-2/260/260" width="260" height="260" alt="Neon-lit alleyway">
</a>
<a class="absolute left-1/2 top-1/2 -ml-[75px] -mt-[75px] size-37.5 overflow-hidden rounded-[14px] shadow-[0_16px_34px_-18px_rgba(0,0,0,0.8)] [transform:translate(var(--x),var(--y))_rotate(var(--r))] transition-[transform,filter] duration-450 ease-stk19 group-has-hover:brightness-50 group-has-hover:[transform:translate(calc(var(--x)*0.3),calc(var(--y)*0.3))_rotate(0deg)] group-has-focus-visible:brightness-50 group-has-focus-visible:[transform:translate(calc(var(--x)*0.3),calc(var(--y)*0.3))_rotate(0deg)] hover:z-10 hover:brightness-110! hover:[transform:translate(0,0)_rotate(0deg)_scale(1.25)]! focus-visible:z-10 focus-visible:outline-none focus-visible:brightness-110! focus-visible:[transform:translate(0,0)_rotate(0deg)_scale(1.25)]! focus-visible:shadow-[0_0_0_3px_var(--color-stk19-ring),0_24px_44px_-18px_rgba(0,0,0,0.9)] motion-reduce:group-has-hover:[transform:translate(var(--x),var(--y))_rotate(var(--r))] motion-reduce:hover:[transform:translate(var(--x),var(--y))_rotate(var(--r))_scale(1.1)]!" href="#" style="--x:-90px;--y:70px;--r:5deg">
<img class="block size-full object-cover" src="https://picsum.photos/seed/tw-pinch-3/260/260" width="260" height="260" alt="Rooftop garden in summer">
</a>
<a class="absolute left-1/2 top-1/2 -ml-[75px] -mt-[75px] size-37.5 overflow-hidden rounded-[14px] shadow-[0_16px_34px_-18px_rgba(0,0,0,0.8)] [transform:translate(var(--x),var(--y))_rotate(var(--r))] transition-[transform,filter] duration-450 ease-stk19 group-has-hover:brightness-50 group-has-hover:[transform:translate(calc(var(--x)*0.3),calc(var(--y)*0.3))_rotate(0deg)] group-has-focus-visible:brightness-50 group-has-focus-visible:[transform:translate(calc(var(--x)*0.3),calc(var(--y)*0.3))_rotate(0deg)] hover:z-10 hover:brightness-110! hover:[transform:translate(0,0)_rotate(0deg)_scale(1.25)]! focus-visible:z-10 focus-visible:outline-none focus-visible:brightness-110! focus-visible:[transform:translate(0,0)_rotate(0deg)_scale(1.25)]! focus-visible:shadow-[0_0_0_3px_var(--color-stk19-ring),0_24px_44px_-18px_rgba(0,0,0,0.9)] motion-reduce:group-has-hover:[transform:translate(var(--x),var(--y))_rotate(var(--r))] motion-reduce:hover:[transform:translate(var(--x),var(--y))_rotate(var(--r))_scale(1.1)]!" href="#" style="--x:120px;--y:60px;--r:-6deg">
<img class="block size-full object-cover" src="https://picsum.photos/seed/tw-pinch-4/260/260" width="260" height="260" alt="Harbour lights reflected on water">
</a>
<a class="absolute left-1/2 top-1/2 -ml-[75px] -mt-[75px] size-37.5 overflow-hidden rounded-[14px] shadow-[0_16px_34px_-18px_rgba(0,0,0,0.8)] [transform:translate(var(--x),var(--y))_rotate(var(--r))] transition-[transform,filter] duration-450 ease-stk19 group-has-hover:brightness-50 group-has-hover:[transform:translate(calc(var(--x)*0.3),calc(var(--y)*0.3))_rotate(0deg)] group-has-focus-visible:brightness-50 group-has-focus-visible:[transform:translate(calc(var(--x)*0.3),calc(var(--y)*0.3))_rotate(0deg)] hover:z-10 hover:brightness-110! hover:[transform:translate(0,0)_rotate(0deg)_scale(1.25)]! focus-visible:z-10 focus-visible:outline-none focus-visible:brightness-110! focus-visible:[transform:translate(0,0)_rotate(0deg)_scale(1.25)]! focus-visible:shadow-[0_0_0_3px_var(--color-stk19-ring),0_24px_44px_-18px_rgba(0,0,0,0.9)] motion-reduce:group-has-hover:[transform:translate(var(--x),var(--y))_rotate(var(--r))] motion-reduce:hover:[transform:translate(var(--x),var(--y))_rotate(var(--r))_scale(1.1)]!" href="#" style="--x:0px;--y:0px;--r:2deg">
<img class="block size-full object-cover" src="https://picsum.photos/seed/tw-pinch-5/260/260" width="260" height="260" alt="Landscape blurred past a train window">
</a>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-stk19-bg: #0c0d13;
--color-stk19-bg: oklch(0.14 0.012 275);
--color-stk19-ring: #5aa8e8;
--color-stk19-ring: oklch(0.70 0.15 250);
--font-stk19-sans: 'Archivo', system-ui, sans-serif;
--ease-stk19: cubic-bezier(0.3, 0.9, 0.3, 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 Scattered Photos Pinch to Centre on Hover
Source: https://codefronts.com/components/tailwind-stacked-cards/tailwind-scattered-photos-pinch-to-centre-on-hover/
Hover one photo and the whole cluster gathers toward the centre and dims, while the one you touched brightens and rises. The group behaviour is group-has-hover: — Tailwind's variant form of :has(:hover) — and the spotlight override needs the ! modifier, because the hovered photo has to beat a rule its own parent just applied to it.
## HTML
```html
<section class="stk-19 w-full min-h-svh flex items-center justify-center px-5 py-15 font-stk19-sans bg-stk19-bg" aria-label="Pinch-to-focus photo cluster">
<div class="group relative w-[min(360px,90vw)] h-85">
<a class="absolute left-1/2 top-1/2 -ml-[75px] -mt-[75px] size-37.5 overflow-hidden rounded-[14px] shadow-[0_16px_34px_-18px_rgba(0,0,0,0.8)] [transform:translate(var(--x),var(--y))_rotate(var(--r))] transition-[transform,filter] duration-450 ease-stk19 group-has-hover:brightness-50 group-has-hover:[transform:translate(calc(var(--x)*0.3),calc(var(--y)*0.3))_rotate(0deg)] group-has-focus-visible:brightness-50 group-has-focus-visible:[transform:translate(calc(var(--x)*0.3),calc(var(--y)*0.3))_rotate(0deg)] hover:z-10 hover:brightness-110! hover:[transform:translate(0,0)_rotate(0deg)_scale(1.25)]! focus-visible:z-10 focus-visible:outline-none focus-visible:brightness-110! focus-visible:[transform:translate(0,0)_rotate(0deg)_scale(1.25)]! focus-visible:shadow-[0_0_0_3px_var(--color-stk19-ring),0_24px_44px_-18px_rgba(0,0,0,0.9)] motion-reduce:group-has-hover:[transform:translate(var(--x),var(--y))_rotate(var(--r))] motion-reduce:hover:[transform:translate(var(--x),var(--y))_rotate(var(--r))_scale(1.1)]!" href="#" style="--x:-120px;--y:-40px;--r:-7deg">
<img class="block size-full object-cover" src="https://picsum.photos/seed/tw-pinch-1/260/260" width="260" height="260" alt="Street market stall at night">
</a>
<a class="absolute left-1/2 top-1/2 -ml-[75px] -mt-[75px] size-37.5 overflow-hidden rounded-[14px] shadow-[0_16px_34px_-18px_rgba(0,0,0,0.8)] [transform:translate(var(--x),var(--y))_rotate(var(--r))] transition-[transform,filter] duration-450 ease-stk19 group-has-hover:brightness-50 group-has-hover:[transform:translate(calc(var(--x)*0.3),calc(var(--y)*0.3))_rotate(0deg)] group-has-focus-visible:brightness-50 group-has-focus-visible:[transform:translate(calc(var(--x)*0.3),calc(var(--y)*0.3))_rotate(0deg)] hover:z-10 hover:brightness-110! hover:[transform:translate(0,0)_rotate(0deg)_scale(1.25)]! focus-visible:z-10 focus-visible:outline-none focus-visible:brightness-110! focus-visible:[transform:translate(0,0)_rotate(0deg)_scale(1.25)]! focus-visible:shadow-[0_0_0_3px_var(--color-stk19-ring),0_24px_44px_-18px_rgba(0,0,0,0.9)] motion-reduce:group-has-hover:[transform:translate(var(--x),var(--y))_rotate(var(--r))] motion-reduce:hover:[transform:translate(var(--x),var(--y))_rotate(var(--r))_scale(1.1)]!" href="#" style="--x:110px;--y:-60px;--r:6deg">
<img class="block size-full object-cover" src="https://picsum.photos/seed/tw-pinch-2/260/260" width="260" height="260" alt="Neon-lit alleyway">
</a>
<a class="absolute left-1/2 top-1/2 -ml-[75px] -mt-[75px] size-37.5 overflow-hidden rounded-[14px] shadow-[0_16px_34px_-18px_rgba(0,0,0,0.8)] [transform:translate(var(--x),var(--y))_rotate(var(--r))] transition-[transform,filter] duration-450 ease-stk19 group-has-hover:brightness-50 group-has-hover:[transform:translate(calc(var(--x)*0.3),calc(var(--y)*0.3))_rotate(0deg)] group-has-focus-visible:brightness-50 group-has-focus-visible:[transform:translate(calc(var(--x)*0.3),calc(var(--y)*0.3))_rotate(0deg)] hover:z-10 hover:brightness-110! hover:[transform:translate(0,0)_rotate(0deg)_scale(1.25)]! focus-visible:z-10 focus-visible:outline-none focus-visible:brightness-110! focus-visible:[transform:translate(0,0)_rotate(0deg)_scale(1.25)]! focus-visible:shadow-[0_0_0_3px_var(--color-stk19-ring),0_24px_44px_-18px_rgba(0,0,0,0.9)] motion-reduce:group-has-hover:[transform:translate(var(--x),var(--y))_rotate(var(--r))] motion-reduce:hover:[transform:translate(var(--x),var(--y))_rotate(var(--r))_scale(1.1)]!" href="#" style="--x:-90px;--y:70px;--r:5deg">
<img class="block size-full object-cover" src="https://picsum.photos/seed/tw-pinch-3/260/260" width="260" height="260" alt="Rooftop garden in summer">
</a>
<a class="absolute left-1/2 top-1/2 -ml-[75px] -mt-[75px] size-37.5 overflow-hidden rounded-[14px] shadow-[0_16px_34px_-18px_rgba(0,0,0,0.8)] [transform:translate(var(--x),var(--y))_rotate(var(--r))] transition-[transform,filter] duration-450 ease-stk19 group-has-hover:brightness-50 group-has-hover:[transform:translate(calc(var(--x)*0.3),calc(var(--y)*0.3))_rotate(0deg)] group-has-focus-visible:brightness-50 group-has-focus-visible:[transform:translate(calc(var(--x)*0.3),calc(var(--y)*0.3))_rotate(0deg)] hover:z-10 hover:brightness-110! hover:[transform:translate(0,0)_rotate(0deg)_scale(1.25)]! focus-visible:z-10 focus-visible:outline-none focus-visible:brightness-110! focus-visible:[transform:translate(0,0)_rotate(0deg)_scale(1.25)]! focus-visible:shadow-[0_0_0_3px_var(--color-stk19-ring),0_24px_44px_-18px_rgba(0,0,0,0.9)] motion-reduce:group-has-hover:[transform:translate(var(--x),var(--y))_rotate(var(--r))] motion-reduce:hover:[transform:translate(var(--x),var(--y))_rotate(var(--r))_scale(1.1)]!" href="#" style="--x:120px;--y:60px;--r:-6deg">
<img class="block size-full object-cover" src="https://picsum.photos/seed/tw-pinch-4/260/260" width="260" height="260" alt="Harbour lights reflected on water">
</a>
<a class="absolute left-1/2 top-1/2 -ml-[75px] -mt-[75px] size-37.5 overflow-hidden rounded-[14px] shadow-[0_16px_34px_-18px_rgba(0,0,0,0.8)] [transform:translate(var(--x),var(--y))_rotate(var(--r))] transition-[transform,filter] duration-450 ease-stk19 group-has-hover:brightness-50 group-has-hover:[transform:translate(calc(var(--x)*0.3),calc(var(--y)*0.3))_rotate(0deg)] group-has-focus-visible:brightness-50 group-has-focus-visible:[transform:translate(calc(var(--x)*0.3),calc(var(--y)*0.3))_rotate(0deg)] hover:z-10 hover:brightness-110! hover:[transform:translate(0,0)_rotate(0deg)_scale(1.25)]! focus-visible:z-10 focus-visible:outline-none focus-visible:brightness-110! focus-visible:[transform:translate(0,0)_rotate(0deg)_scale(1.25)]! focus-visible:shadow-[0_0_0_3px_var(--color-stk19-ring),0_24px_44px_-18px_rgba(0,0,0,0.9)] motion-reduce:group-has-hover:[transform:translate(var(--x),var(--y))_rotate(var(--r))] motion-reduce:hover:[transform:translate(var(--x),var(--y))_rotate(var(--r))_scale(1.1)]!" href="#" style="--x:0px;--y:0px;--r:2deg">
<img class="block size-full object-cover" src="https://picsum.photos/seed/tw-pinch-5/260/260" width="260" height="260" alt="Landscape blurred past a train window">
</a>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-stk19-bg: #0c0d13;
--color-stk19-bg: oklch(0.14 0.012 275);
--color-stk19-ring: #5aa8e8;
--color-stk19-ring: oklch(0.70 0.15 250);
--font-stk19-sans: 'Archivo', system-ui, sans-serif;
--ease-stk19: cubic-bezier(0.3, 0.9, 0.3, 1);
}
```How this works
:has() is a variant now, and it changes how this is written. The CSS version needs .cluster:has(.ph:hover) .ph. Tailwind expresses the same thing from the child, with the cluster marked group:
group-has-hover:[transform:translate(calc(var(--x)*0.3),calc(var(--y)*0.3))_rotate(0deg)]
group-has-hover:brightness-50Multiplying the existing scatter by 0.3 rather than moving to fixed coordinates is what makes the pinch look magnetic: every photo travels toward the centre along its own vector, proportionally.
Then the spotlight has to fight its own parent. The hovered photo is also matched by group-has-hover:, at identical specificity. Without the important modifier the pack rule may win and the photo you are pointing at dims with the rest:
hover:[transform:translate(0,0)_rotate(0deg)_scale(1.25)]!
hover:brightness-110!This is the second of the collection's two legitimate uses of !, and both are the same shape of problem: a narrower state overriding a broader one that utilities cannot out-specify.
Graceful without :has(). Where :has() is missing the group pinch simply does not apply, and each photo still brightens and lifts on its own hover — the individual state is a separate variant and does not depend on it.
Make it yours
- Set the pinch strength with the
0.3multiplier —0gathers everything to dead centre,0.6is a gentle nudge. - Change the spotlight balance with
group-has-hover:brightness-50againsthover:brightness-110!. - Move the gathering point by adding an offset inside the pinch transform.
- Reposition any photo with its own three numbers; the pinch is derived from them, so it follows automatically.
- Restyle the frames —
rounded-[14px]and the fixedsize-37.5are the only geometry.
Gotchas — read before shipping
- The hovered photo's override needs the
!modifier. It is matched by the group rule too, and utilities give you no way to be more specific. :has()needs Chrome 105+, Safari 15.4+, Firefox 121+. Keep the individual hover state independent so the demo degrades to per-photo lift.- Keep meaningful
alttext. A decorative arrangement is not a reason to drop content from the accessibility tree. - The spotlit photo needs
z-10; scale alone does not lift it above later siblings. - Under reduced motion, freeze the scatter and keep the brightness change — a filter is not vestibular, a five-element convergence is.
brightness-50on a photo is a real accessibility consideration if the image carries information; here the caption-free cluster is illustrative, so it is safe.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
:has() is Chrome 105+, Safari 15.4+, Firefox 121+ and drives the group pinch; where it is missing each photo still brightens and lifts on its own hover. Transforms and filters are universal.