16 / 22
All 22 in 22 Tailwind CSS Image Gallery
- 01Tailwind Hexagon Honeycomb Image Gallery with clip-path
- 02Tailwind Justified Image Gallery with Aspect-Ratio Flex Rows
- 03Tailwind Expanding Panel Image Gallery with Flex-Grow Transitions
- 04Tailwind :has() Hover Spotlight Image Grid
- 05Tailwind Radial Orbit Image Gallery with Counter-Rotating Thumbnails
- 06Tailwind Interactive Pointer Trail Image Gallery
- 07Tailwind Masonry Image Gallery with CSS Columns (No JavaScript)
- 08Tailwind Metro Tile Grid Gallery with Dense Auto-Flow
- 09Tailwind Editorial Magazine Grid Gallery with Blend-Mode Headline
- 10Tailwind Pure CSS Lightbox Gallery with :target (No JavaScript)
- 11Tailwind Filmstrip Carousel Gallery with CSS Scroll-Snap
- 12Tailwind Accordion Image Gallery with Flex-Grow on Hover & Focus
- 13Tailwind Mosaic Grid Gallery with Contained Zoom on Hover
- 14Tailwind Glassmorphism Image Cards with backdrop-blur
- 15Tailwind Diagonal Slice Reveal Gallery with clip-path Halves
- 16Tailwind 3D Tilt Parallax Image Cards with Pointer-Driven Variables
- 17Tailwind 3D Cube Image Viewer with Radio-Driven Rotation
- 18Tailwind 3D Perspective Depth Stack Gallery That Fans on Focus
- 19Tailwind Infinite Scrolling Image Marquee with Seamless Loop
- 20Tailwind Ken Burns Slideshow Gallery with Staggered Cross-Fade
- 21Tailwind Polaroid Photo Stack Gallery with Rotate-on-Hover
- 22Tailwind Scattered Photo Collage with :target Spotlight
Tailwind 3D Tilt Parallax Image Cards with Pointer-Driven Variables
Photo cards that tilt in 3D toward the cursor with a moving specular glare and a caption floating above the image. JavaScript writes four numbers; every visual consequence is a Tailwind utility reading them — the cleanest possible division between behaviour and appearance.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="tgl-16 w-full min-h-svh grid place-items-center px-5 py-10 font-tgl16-sans [perspective:1000px] bg-[radial-gradient(120%_120%_at_50%_15%,oklch(0.2_0.04_290),oklch(0.09_0.02_280))]" id="tgl-16" aria-label="3D tilt parallax cards">
<div class="flex flex-wrap justify-center gap-7 w-[min(940px,100%)]">
<article data-tilt tabindex="0" class="group relative w-[260px] rounded-[20px] overflow-hidden cursor-pointer bg-tgl16-well [--rx:0deg] [--ry:0deg] [--mx:50%] [--my:50%] [transform:rotateX(var(--rx))_rotateY(var(--ry))] transform-3d transition-[transform,box-shadow] duration-[400ms] shadow-[0_30px_60px_-30px_rgba(0,0,0,0.8)] after:content-[''] after:absolute after:inset-0 after:z-[3] after:pointer-events-none after:opacity-0 after:transition-opacity after:duration-300 after:mix-blend-screen after:bg-[radial-gradient(300px_300px_at_var(--mx)_var(--my),rgba(255,255,255,0.28),transparent_60%)] hover:after:opacity-100 focus-visible:outline-3 focus-visible:outline-tgl16-acc focus-visible:outline-offset-[3px] motion-reduce:[transform:none] motion-reduce:transition-none motion-reduce:after:hidden">
<div class="overflow-hidden"><img src="https://images.unsplash.com/photo-1516031190212-da133013de50?q=80&w=500&h=620&auto=format&fit=crop" alt="Aurora over mountains" loading="lazy" decoding="async" class="block w-full [aspect-ratio:5/6] object-cover"></div>
<figcaption class="absolute inset-x-0 bottom-0 z-[2] px-5 py-5 [transform:translateZ(50px)] bg-[linear-gradient(0deg,rgba(8,6,15,0.9),transparent)] motion-reduce:[transform:none]">
<span class="font-tgl16-mono text-[11px] tracking-[0.2em] uppercase text-tgl16-acc">Iceland</span>
<h3 class="mt-1 text-[19px] font-bold text-white">Northern Skies</h3>
</figcaption>
</article>
<article data-tilt tabindex="0" class="group relative w-[260px] rounded-[20px] overflow-hidden cursor-pointer bg-tgl16-well [--rx:0deg] [--ry:0deg] [--mx:50%] [--my:50%] [transform:rotateX(var(--rx))_rotateY(var(--ry))] transform-3d transition-[transform,box-shadow] duration-[400ms] shadow-[0_30px_60px_-30px_rgba(0,0,0,0.8)] after:content-[''] after:absolute after:inset-0 after:z-[3] after:pointer-events-none after:opacity-0 after:transition-opacity after:duration-300 after:mix-blend-screen after:bg-[radial-gradient(300px_300px_at_var(--mx)_var(--my),rgba(255,255,255,0.28),transparent_60%)] hover:after:opacity-100 focus-visible:outline-3 focus-visible:outline-tgl16-acc focus-visible:outline-offset-[3px] motion-reduce:[transform:none] motion-reduce:transition-none motion-reduce:after:hidden">
<div class="overflow-hidden"><img src="https://images.unsplash.com/photo-1483985988355-763728e1935b?q=80&w=500&h=620&auto=format&fit=crop" alt="Canyon at sunset" loading="lazy" decoding="async" class="block w-full [aspect-ratio:5/6] object-cover"></div>
<figcaption class="absolute inset-x-0 bottom-0 z-[2] px-5 py-5 [transform:translateZ(50px)] bg-[linear-gradient(0deg,rgba(8,6,15,0.9),transparent)] motion-reduce:[transform:none]">
<span class="font-tgl16-mono text-[11px] tracking-[0.2em] uppercase text-tgl16-acc">Utah</span>
<h3 class="mt-1 text-[19px] font-bold text-white">Red Horizon</h3>
</figcaption>
</article>
<article data-tilt tabindex="0" class="group relative w-[260px] rounded-[20px] overflow-hidden cursor-pointer bg-tgl16-well [--rx:0deg] [--ry:0deg] [--mx:50%] [--my:50%] [transform:rotateX(var(--rx))_rotateY(var(--ry))] transform-3d transition-[transform,box-shadow] duration-[400ms] shadow-[0_30px_60px_-30px_rgba(0,0,0,0.8)] after:content-[''] after:absolute after:inset-0 after:z-[3] after:pointer-events-none after:opacity-0 after:transition-opacity after:duration-300 after:mix-blend-screen after:bg-[radial-gradient(300px_300px_at_var(--mx)_var(--my),rgba(255,255,255,0.28),transparent_60%)] hover:after:opacity-100 focus-visible:outline-3 focus-visible:outline-tgl16-acc focus-visible:outline-offset-[3px] motion-reduce:[transform:none] motion-reduce:transition-none motion-reduce:after:hidden">
<div class="overflow-hidden"><img src="https://images.unsplash.com/photo-1620799140408-edc6dcb6d633?q=80&w=500&h=620&auto=format&fit=crop" alt="Foggy forest at dawn" loading="lazy" decoding="async" class="block w-full [aspect-ratio:5/6] object-cover"></div>
<figcaption class="absolute inset-x-0 bottom-0 z-[2] px-5 py-5 [transform:translateZ(50px)] bg-[linear-gradient(0deg,rgba(8,6,15,0.9),transparent)] motion-reduce:[transform:none]">
<span class="font-tgl16-mono text-[11px] tracking-[0.2em] uppercase text-tgl16-acc">Oregon</span>
<h3 class="mt-1 text-[19px] font-bold text-white">Deep Green</h3>
</figcaption>
</article>
</div>
</section><section class="tgl-16 w-full min-h-svh grid place-items-center px-5 py-10 font-tgl16-sans [perspective:1000px] bg-[radial-gradient(120%_120%_at_50%_15%,oklch(0.2_0.04_290),oklch(0.09_0.02_280))]" id="tgl-16" aria-label="3D tilt parallax cards">
<div class="flex flex-wrap justify-center gap-7 w-[min(940px,100%)]">
<article data-tilt tabindex="0" class="group relative w-[260px] rounded-[20px] overflow-hidden cursor-pointer bg-tgl16-well [--rx:0deg] [--ry:0deg] [--mx:50%] [--my:50%] [transform:rotateX(var(--rx))_rotateY(var(--ry))] transform-3d transition-[transform,box-shadow] duration-[400ms] shadow-[0_30px_60px_-30px_rgba(0,0,0,0.8)] after:content-[''] after:absolute after:inset-0 after:z-[3] after:pointer-events-none after:opacity-0 after:transition-opacity after:duration-300 after:mix-blend-screen after:bg-[radial-gradient(300px_300px_at_var(--mx)_var(--my),rgba(255,255,255,0.28),transparent_60%)] hover:after:opacity-100 focus-visible:outline-3 focus-visible:outline-tgl16-acc focus-visible:outline-offset-[3px] motion-reduce:[transform:none] motion-reduce:transition-none motion-reduce:after:hidden">
<div class="overflow-hidden"><img src="https://images.unsplash.com/photo-1516031190212-da133013de50?q=80&w=500&h=620&auto=format&fit=crop" alt="Aurora over mountains" loading="lazy" decoding="async" class="block w-full [aspect-ratio:5/6] object-cover"></div>
<figcaption class="absolute inset-x-0 bottom-0 z-[2] px-5 py-5 [transform:translateZ(50px)] bg-[linear-gradient(0deg,rgba(8,6,15,0.9),transparent)] motion-reduce:[transform:none]">
<span class="font-tgl16-mono text-[11px] tracking-[0.2em] uppercase text-tgl16-acc">Iceland</span>
<h3 class="mt-1 text-[19px] font-bold text-white">Northern Skies</h3>
</figcaption>
</article>
<article data-tilt tabindex="0" class="group relative w-[260px] rounded-[20px] overflow-hidden cursor-pointer bg-tgl16-well [--rx:0deg] [--ry:0deg] [--mx:50%] [--my:50%] [transform:rotateX(var(--rx))_rotateY(var(--ry))] transform-3d transition-[transform,box-shadow] duration-[400ms] shadow-[0_30px_60px_-30px_rgba(0,0,0,0.8)] after:content-[''] after:absolute after:inset-0 after:z-[3] after:pointer-events-none after:opacity-0 after:transition-opacity after:duration-300 after:mix-blend-screen after:bg-[radial-gradient(300px_300px_at_var(--mx)_var(--my),rgba(255,255,255,0.28),transparent_60%)] hover:after:opacity-100 focus-visible:outline-3 focus-visible:outline-tgl16-acc focus-visible:outline-offset-[3px] motion-reduce:[transform:none] motion-reduce:transition-none motion-reduce:after:hidden">
<div class="overflow-hidden"><img src="https://images.unsplash.com/photo-1483985988355-763728e1935b?q=80&w=500&h=620&auto=format&fit=crop" alt="Canyon at sunset" loading="lazy" decoding="async" class="block w-full [aspect-ratio:5/6] object-cover"></div>
<figcaption class="absolute inset-x-0 bottom-0 z-[2] px-5 py-5 [transform:translateZ(50px)] bg-[linear-gradient(0deg,rgba(8,6,15,0.9),transparent)] motion-reduce:[transform:none]">
<span class="font-tgl16-mono text-[11px] tracking-[0.2em] uppercase text-tgl16-acc">Utah</span>
<h3 class="mt-1 text-[19px] font-bold text-white">Red Horizon</h3>
</figcaption>
</article>
<article data-tilt tabindex="0" class="group relative w-[260px] rounded-[20px] overflow-hidden cursor-pointer bg-tgl16-well [--rx:0deg] [--ry:0deg] [--mx:50%] [--my:50%] [transform:rotateX(var(--rx))_rotateY(var(--ry))] transform-3d transition-[transform,box-shadow] duration-[400ms] shadow-[0_30px_60px_-30px_rgba(0,0,0,0.8)] after:content-[''] after:absolute after:inset-0 after:z-[3] after:pointer-events-none after:opacity-0 after:transition-opacity after:duration-300 after:mix-blend-screen after:bg-[radial-gradient(300px_300px_at_var(--mx)_var(--my),rgba(255,255,255,0.28),transparent_60%)] hover:after:opacity-100 focus-visible:outline-3 focus-visible:outline-tgl16-acc focus-visible:outline-offset-[3px] motion-reduce:[transform:none] motion-reduce:transition-none motion-reduce:after:hidden">
<div class="overflow-hidden"><img src="https://images.unsplash.com/photo-1620799140408-edc6dcb6d633?q=80&w=500&h=620&auto=format&fit=crop" alt="Foggy forest at dawn" loading="lazy" decoding="async" class="block w-full [aspect-ratio:5/6] object-cover"></div>
<figcaption class="absolute inset-x-0 bottom-0 z-[2] px-5 py-5 [transform:translateZ(50px)] bg-[linear-gradient(0deg,rgba(8,6,15,0.9),transparent)] motion-reduce:[transform:none]">
<span class="font-tgl16-mono text-[11px] tracking-[0.2em] uppercase text-tgl16-acc">Oregon</span>
<h3 class="mt-1 text-[19px] font-bold text-white">Deep Green</h3>
</figcaption>
</article>
</div>
</section>@import "tailwindcss";
/* @property registration has no utility form — typed variables are what make the
settle-back on pointer-leave interpolate instead of snapping. */
@property --rx {
syntax: '<angle>';
inherits: true;
initial-value: 0deg;
}
@property --ry {
syntax: '<angle>';
inherits: true;
initial-value: 0deg;
}
@theme {
--color-tgl16-well: #12101d;
--color-tgl16-well: oklch(0.19 0.02 285);
--color-tgl16-acc: #b9a7f7;
--color-tgl16-acc: oklch(0.78 0.13 290);
--font-tgl16-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tgl16-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
}
.tgl-16 {
width: 100%;
min-height: 100svh;
}
.tgl-16 ::selection {
background: var(--color-tgl16-acc);
color: #0a0616;
}@import "tailwindcss";
/* @property registration has no utility form — typed variables are what make the
settle-back on pointer-leave interpolate instead of snapping. */
@property --rx {
syntax: '<angle>';
inherits: true;
initial-value: 0deg;
}
@property --ry {
syntax: '<angle>';
inherits: true;
initial-value: 0deg;
}
@theme {
--color-tgl16-well: #12101d;
--color-tgl16-well: oklch(0.19 0.02 285);
--color-tgl16-acc: #b9a7f7;
--color-tgl16-acc: oklch(0.78 0.13 290);
--font-tgl16-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tgl16-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
}
.tgl-16 {
width: 100%;
min-height: 100svh;
}
.tgl-16 ::selection {
background: var(--color-tgl16-acc);
color: #0a0616;
}(function () {
var root = document.getElementById('tgl-16');
if (!root) return;
if (matchMedia('(hover:none)').matches || matchMedia('(prefers-reduced-motion:reduce)').matches) return;
var MAX = 12; // max tilt in degrees
root.querySelectorAll('[data-tilt]').forEach(function (card) {
card.addEventListener('pointermove', function (e) {
var r = card.getBoundingClientRect();
var px = (e.clientX - r.left) / r.width;
var py = (e.clientY - r.top) / r.height;
card.style.setProperty('--ry', ((px - 0.5) * 2 * MAX).toFixed(2) + 'deg');
card.style.setProperty('--rx', ((0.5 - py) * 2 * MAX).toFixed(2) + 'deg');
card.style.setProperty('--mx', (px * 100).toFixed(1) + '%');
card.style.setProperty('--my', (py * 100).toFixed(1) + '%');
});
card.addEventListener('pointerleave', function () {
card.style.setProperty('--rx', '0deg');
card.style.setProperty('--ry', '0deg');
});
});
})();(function () {
var root = document.getElementById('tgl-16');
if (!root) return;
if (matchMedia('(hover:none)').matches || matchMedia('(prefers-reduced-motion:reduce)').matches) return;
var MAX = 12; // max tilt in degrees
root.querySelectorAll('[data-tilt]').forEach(function (card) {
card.addEventListener('pointermove', function (e) {
var r = card.getBoundingClientRect();
var px = (e.clientX - r.left) / r.width;
var py = (e.clientY - r.top) / r.height;
card.style.setProperty('--ry', ((px - 0.5) * 2 * MAX).toFixed(2) + 'deg');
card.style.setProperty('--rx', ((0.5 - py) * 2 * MAX).toFixed(2) + 'deg');
card.style.setProperty('--mx', (px * 100).toFixed(1) + '%');
card.style.setProperty('--my', (py * 100).toFixed(1) + '%');
});
card.addEventListener('pointerleave', function () {
card.style.setProperty('--rx', '0deg');
card.style.setProperty('--ry', '0deg');
});
});
})();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";
/* @property registration has no utility form — typed variables are what make the
settle-back on pointer-leave interpolate instead of snapping. */
@property --rx {
syntax: '<angle>';
inherits: true;
initial-value: 0deg;
}
@property --ry {
syntax: '<angle>';
inherits: true;
initial-value: 0deg;
}
@theme {
--color-tgl16-well: #12101d;
--color-tgl16-well: oklch(0.19 0.02 285);
--color-tgl16-acc: #b9a7f7;
--color-tgl16-acc: oklch(0.78 0.13 290);
--font-tgl16-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tgl16-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
}
.tgl-16 {
width: 100%;
min-height: 100svh;
}
.tgl-16 ::selection {
background: var(--color-tgl16-acc);
color: #0a0616;
}@import "tailwindcss";
/* @property registration has no utility form — typed variables are what make the
settle-back on pointer-leave interpolate instead of snapping. */
@property --rx {
syntax: '<angle>';
inherits: true;
initial-value: 0deg;
}
@property --ry {
syntax: '<angle>';
inherits: true;
initial-value: 0deg;
}
@theme {
--color-tgl16-well: #12101d;
--color-tgl16-well: oklch(0.19 0.02 285);
--color-tgl16-acc: #b9a7f7;
--color-tgl16-acc: oklch(0.78 0.13 290);
--font-tgl16-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tgl16-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
}
.tgl-16 {
width: 100%;
min-height: 100svh;
}
.tgl-16 ::selection {
background: var(--color-tgl16-acc);
color: #0a0616;
}<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">
<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.
- The four variables are declared as arbitrary properties in the class list (
[--rx:0deg]etc.), so a reader can see the card's full interface without opening the script. transform-3dis a real v4 utility fortransform-style: preserve-3d; the tilt and thetranslateZcaption both depend on it and it is easy to forget.- UI change from the CSS original: the cards are now
tabindex="0"with a visible focus ring. The original styled:focus-visibleon an element that could never receive focus, so the rule was dead.
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 Tilt Parallax Image Cards with Pointer-Driven Variables
Source: https://codefronts.com/snippets/tailwind-css-image-gallery/tailwind-3d-tilt-parallax-image-cards/
Photo cards that tilt in 3D toward the cursor with a moving specular glare and a caption floating above the image. JavaScript writes four numbers; every visual consequence is a Tailwind utility reading them — the cleanest possible division between behaviour and appearance.
## HTML
```html
<section class="tgl-16 w-full min-h-svh grid place-items-center px-5 py-10 font-tgl16-sans [perspective:1000px] bg-[radial-gradient(120%_120%_at_50%_15%,oklch(0.2_0.04_290),oklch(0.09_0.02_280))]" id="tgl-16" aria-label="3D tilt parallax cards">
<div class="flex flex-wrap justify-center gap-7 w-[min(940px,100%)]">
<article data-tilt tabindex="0" class="group relative w-[260px] rounded-[20px] overflow-hidden cursor-pointer bg-tgl16-well [--rx:0deg] [--ry:0deg] [--mx:50%] [--my:50%] [transform:rotateX(var(--rx))_rotateY(var(--ry))] transform-3d transition-[transform,box-shadow] duration-[400ms] shadow-[0_30px_60px_-30px_rgba(0,0,0,0.8)] after:content-[''] after:absolute after:inset-0 after:z-[3] after:pointer-events-none after:opacity-0 after:transition-opacity after:duration-300 after:mix-blend-screen after:bg-[radial-gradient(300px_300px_at_var(--mx)_var(--my),rgba(255,255,255,0.28),transparent_60%)] hover:after:opacity-100 focus-visible:outline-3 focus-visible:outline-tgl16-acc focus-visible:outline-offset-[3px] motion-reduce:[transform:none] motion-reduce:transition-none motion-reduce:after:hidden">
<div class="overflow-hidden"><img src="https://images.unsplash.com/photo-1516031190212-da133013de50?q=80&w=500&h=620&auto=format&fit=crop" alt="Aurora over mountains" loading="lazy" decoding="async" class="block w-full [aspect-ratio:5/6] object-cover"></div>
<figcaption class="absolute inset-x-0 bottom-0 z-[2] px-5 py-5 [transform:translateZ(50px)] bg-[linear-gradient(0deg,rgba(8,6,15,0.9),transparent)] motion-reduce:[transform:none]">
<span class="font-tgl16-mono text-[11px] tracking-[0.2em] uppercase text-tgl16-acc">Iceland</span>
<h3 class="mt-1 text-[19px] font-bold text-white">Northern Skies</h3>
</figcaption>
</article>
<article data-tilt tabindex="0" class="group relative w-[260px] rounded-[20px] overflow-hidden cursor-pointer bg-tgl16-well [--rx:0deg] [--ry:0deg] [--mx:50%] [--my:50%] [transform:rotateX(var(--rx))_rotateY(var(--ry))] transform-3d transition-[transform,box-shadow] duration-[400ms] shadow-[0_30px_60px_-30px_rgba(0,0,0,0.8)] after:content-[''] after:absolute after:inset-0 after:z-[3] after:pointer-events-none after:opacity-0 after:transition-opacity after:duration-300 after:mix-blend-screen after:bg-[radial-gradient(300px_300px_at_var(--mx)_var(--my),rgba(255,255,255,0.28),transparent_60%)] hover:after:opacity-100 focus-visible:outline-3 focus-visible:outline-tgl16-acc focus-visible:outline-offset-[3px] motion-reduce:[transform:none] motion-reduce:transition-none motion-reduce:after:hidden">
<div class="overflow-hidden"><img src="https://images.unsplash.com/photo-1483985988355-763728e1935b?q=80&w=500&h=620&auto=format&fit=crop" alt="Canyon at sunset" loading="lazy" decoding="async" class="block w-full [aspect-ratio:5/6] object-cover"></div>
<figcaption class="absolute inset-x-0 bottom-0 z-[2] px-5 py-5 [transform:translateZ(50px)] bg-[linear-gradient(0deg,rgba(8,6,15,0.9),transparent)] motion-reduce:[transform:none]">
<span class="font-tgl16-mono text-[11px] tracking-[0.2em] uppercase text-tgl16-acc">Utah</span>
<h3 class="mt-1 text-[19px] font-bold text-white">Red Horizon</h3>
</figcaption>
</article>
<article data-tilt tabindex="0" class="group relative w-[260px] rounded-[20px] overflow-hidden cursor-pointer bg-tgl16-well [--rx:0deg] [--ry:0deg] [--mx:50%] [--my:50%] [transform:rotateX(var(--rx))_rotateY(var(--ry))] transform-3d transition-[transform,box-shadow] duration-[400ms] shadow-[0_30px_60px_-30px_rgba(0,0,0,0.8)] after:content-[''] after:absolute after:inset-0 after:z-[3] after:pointer-events-none after:opacity-0 after:transition-opacity after:duration-300 after:mix-blend-screen after:bg-[radial-gradient(300px_300px_at_var(--mx)_var(--my),rgba(255,255,255,0.28),transparent_60%)] hover:after:opacity-100 focus-visible:outline-3 focus-visible:outline-tgl16-acc focus-visible:outline-offset-[3px] motion-reduce:[transform:none] motion-reduce:transition-none motion-reduce:after:hidden">
<div class="overflow-hidden"><img src="https://images.unsplash.com/photo-1620799140408-edc6dcb6d633?q=80&w=500&h=620&auto=format&fit=crop" alt="Foggy forest at dawn" loading="lazy" decoding="async" class="block w-full [aspect-ratio:5/6] object-cover"></div>
<figcaption class="absolute inset-x-0 bottom-0 z-[2] px-5 py-5 [transform:translateZ(50px)] bg-[linear-gradient(0deg,rgba(8,6,15,0.9),transparent)] motion-reduce:[transform:none]">
<span class="font-tgl16-mono text-[11px] tracking-[0.2em] uppercase text-tgl16-acc">Oregon</span>
<h3 class="mt-1 text-[19px] font-bold text-white">Deep Green</h3>
</figcaption>
</article>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
/* @property registration has no utility form — typed variables are what make the
settle-back on pointer-leave interpolate instead of snapping. */
@property --rx {
syntax: '<angle>';
inherits: true;
initial-value: 0deg;
}
@property --ry {
syntax: '<angle>';
inherits: true;
initial-value: 0deg;
}
@theme {
--color-tgl16-well: #12101d;
--color-tgl16-well: oklch(0.19 0.02 285);
--color-tgl16-acc: #b9a7f7;
--color-tgl16-acc: oklch(0.78 0.13 290);
--font-tgl16-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tgl16-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
}
.tgl-16 {
width: 100%;
min-height: 100svh;
}
.tgl-16 ::selection {
background: var(--color-tgl16-acc);
color: #0a0616;
}
```
## JavaScript
```js
(function () {
var root = document.getElementById('tgl-16');
if (!root) return;
if (matchMedia('(hover:none)').matches || matchMedia('(prefers-reduced-motion:reduce)').matches) return;
var MAX = 12; // max tilt in degrees
root.querySelectorAll('[data-tilt]').forEach(function (card) {
card.addEventListener('pointermove', function (e) {
var r = card.getBoundingClientRect();
var px = (e.clientX - r.left) / r.width;
var py = (e.clientY - r.top) / r.height;
card.style.setProperty('--ry', ((px - 0.5) * 2 * MAX).toFixed(2) + 'deg');
card.style.setProperty('--rx', ((0.5 - py) * 2 * MAX).toFixed(2) + 'deg');
card.style.setProperty('--mx', (px * 100).toFixed(1) + '%');
card.style.setProperty('--my', (py * 100).toFixed(1) + '%');
});
card.addEventListener('pointerleave', function () {
card.style.setProperty('--rx', '0deg');
card.style.setProperty('--ry', '0deg');
});
});
})();
```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 Tilt Parallax Image Cards with Pointer-Driven Variables
Source: https://codefronts.com/snippets/tailwind-css-image-gallery/tailwind-3d-tilt-parallax-image-cards/
Photo cards that tilt in 3D toward the cursor with a moving specular glare and a caption floating above the image. JavaScript writes four numbers; every visual consequence is a Tailwind utility reading them — the cleanest possible division between behaviour and appearance.
## HTML
```html
<section class="tgl-16 w-full min-h-svh grid place-items-center px-5 py-10 font-tgl16-sans [perspective:1000px] bg-[radial-gradient(120%_120%_at_50%_15%,oklch(0.2_0.04_290),oklch(0.09_0.02_280))]" id="tgl-16" aria-label="3D tilt parallax cards">
<div class="flex flex-wrap justify-center gap-7 w-[min(940px,100%)]">
<article data-tilt tabindex="0" class="group relative w-[260px] rounded-[20px] overflow-hidden cursor-pointer bg-tgl16-well [--rx:0deg] [--ry:0deg] [--mx:50%] [--my:50%] [transform:rotateX(var(--rx))_rotateY(var(--ry))] transform-3d transition-[transform,box-shadow] duration-[400ms] shadow-[0_30px_60px_-30px_rgba(0,0,0,0.8)] after:content-[''] after:absolute after:inset-0 after:z-[3] after:pointer-events-none after:opacity-0 after:transition-opacity after:duration-300 after:mix-blend-screen after:bg-[radial-gradient(300px_300px_at_var(--mx)_var(--my),rgba(255,255,255,0.28),transparent_60%)] hover:after:opacity-100 focus-visible:outline-3 focus-visible:outline-tgl16-acc focus-visible:outline-offset-[3px] motion-reduce:[transform:none] motion-reduce:transition-none motion-reduce:after:hidden">
<div class="overflow-hidden"><img src="https://images.unsplash.com/photo-1516031190212-da133013de50?q=80&w=500&h=620&auto=format&fit=crop" alt="Aurora over mountains" loading="lazy" decoding="async" class="block w-full [aspect-ratio:5/6] object-cover"></div>
<figcaption class="absolute inset-x-0 bottom-0 z-[2] px-5 py-5 [transform:translateZ(50px)] bg-[linear-gradient(0deg,rgba(8,6,15,0.9),transparent)] motion-reduce:[transform:none]">
<span class="font-tgl16-mono text-[11px] tracking-[0.2em] uppercase text-tgl16-acc">Iceland</span>
<h3 class="mt-1 text-[19px] font-bold text-white">Northern Skies</h3>
</figcaption>
</article>
<article data-tilt tabindex="0" class="group relative w-[260px] rounded-[20px] overflow-hidden cursor-pointer bg-tgl16-well [--rx:0deg] [--ry:0deg] [--mx:50%] [--my:50%] [transform:rotateX(var(--rx))_rotateY(var(--ry))] transform-3d transition-[transform,box-shadow] duration-[400ms] shadow-[0_30px_60px_-30px_rgba(0,0,0,0.8)] after:content-[''] after:absolute after:inset-0 after:z-[3] after:pointer-events-none after:opacity-0 after:transition-opacity after:duration-300 after:mix-blend-screen after:bg-[radial-gradient(300px_300px_at_var(--mx)_var(--my),rgba(255,255,255,0.28),transparent_60%)] hover:after:opacity-100 focus-visible:outline-3 focus-visible:outline-tgl16-acc focus-visible:outline-offset-[3px] motion-reduce:[transform:none] motion-reduce:transition-none motion-reduce:after:hidden">
<div class="overflow-hidden"><img src="https://images.unsplash.com/photo-1483985988355-763728e1935b?q=80&w=500&h=620&auto=format&fit=crop" alt="Canyon at sunset" loading="lazy" decoding="async" class="block w-full [aspect-ratio:5/6] object-cover"></div>
<figcaption class="absolute inset-x-0 bottom-0 z-[2] px-5 py-5 [transform:translateZ(50px)] bg-[linear-gradient(0deg,rgba(8,6,15,0.9),transparent)] motion-reduce:[transform:none]">
<span class="font-tgl16-mono text-[11px] tracking-[0.2em] uppercase text-tgl16-acc">Utah</span>
<h3 class="mt-1 text-[19px] font-bold text-white">Red Horizon</h3>
</figcaption>
</article>
<article data-tilt tabindex="0" class="group relative w-[260px] rounded-[20px] overflow-hidden cursor-pointer bg-tgl16-well [--rx:0deg] [--ry:0deg] [--mx:50%] [--my:50%] [transform:rotateX(var(--rx))_rotateY(var(--ry))] transform-3d transition-[transform,box-shadow] duration-[400ms] shadow-[0_30px_60px_-30px_rgba(0,0,0,0.8)] after:content-[''] after:absolute after:inset-0 after:z-[3] after:pointer-events-none after:opacity-0 after:transition-opacity after:duration-300 after:mix-blend-screen after:bg-[radial-gradient(300px_300px_at_var(--mx)_var(--my),rgba(255,255,255,0.28),transparent_60%)] hover:after:opacity-100 focus-visible:outline-3 focus-visible:outline-tgl16-acc focus-visible:outline-offset-[3px] motion-reduce:[transform:none] motion-reduce:transition-none motion-reduce:after:hidden">
<div class="overflow-hidden"><img src="https://images.unsplash.com/photo-1620799140408-edc6dcb6d633?q=80&w=500&h=620&auto=format&fit=crop" alt="Foggy forest at dawn" loading="lazy" decoding="async" class="block w-full [aspect-ratio:5/6] object-cover"></div>
<figcaption class="absolute inset-x-0 bottom-0 z-[2] px-5 py-5 [transform:translateZ(50px)] bg-[linear-gradient(0deg,rgba(8,6,15,0.9),transparent)] motion-reduce:[transform:none]">
<span class="font-tgl16-mono text-[11px] tracking-[0.2em] uppercase text-tgl16-acc">Oregon</span>
<h3 class="mt-1 text-[19px] font-bold text-white">Deep Green</h3>
</figcaption>
</article>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
/* @property registration has no utility form — typed variables are what make the
settle-back on pointer-leave interpolate instead of snapping. */
@property --rx {
syntax: '<angle>';
inherits: true;
initial-value: 0deg;
}
@property --ry {
syntax: '<angle>';
inherits: true;
initial-value: 0deg;
}
@theme {
--color-tgl16-well: #12101d;
--color-tgl16-well: oklch(0.19 0.02 285);
--color-tgl16-acc: #b9a7f7;
--color-tgl16-acc: oklch(0.78 0.13 290);
--font-tgl16-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tgl16-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
}
.tgl-16 {
width: 100%;
min-height: 100svh;
}
.tgl-16 ::selection {
background: var(--color-tgl16-acc);
color: #0a0616;
}
```
## JavaScript
```js
(function () {
var root = document.getElementById('tgl-16');
if (!root) return;
if (matchMedia('(hover:none)').matches || matchMedia('(prefers-reduced-motion:reduce)').matches) return;
var MAX = 12; // max tilt in degrees
root.querySelectorAll('[data-tilt]').forEach(function (card) {
card.addEventListener('pointermove', function (e) {
var r = card.getBoundingClientRect();
var px = (e.clientX - r.left) / r.width;
var py = (e.clientY - r.top) / r.height;
card.style.setProperty('--ry', ((px - 0.5) * 2 * MAX).toFixed(2) + 'deg');
card.style.setProperty('--rx', ((0.5 - py) * 2 * MAX).toFixed(2) + 'deg');
card.style.setProperty('--mx', (px * 100).toFixed(1) + '%');
card.style.setProperty('--my', (py * 100).toFixed(1) + '%');
});
card.addEventListener('pointerleave', function () {
card.style.setProperty('--rx', '0deg');
card.style.setProperty('--ry', '0deg');
});
});
})();
```How this works
JS supplies numbers, CSS supplies everything else. A pointer handler writes --rx, --ry, --mx and --my; nothing else. The card's transform, the glare position and the eased return are all declared in the class list, so the effect can be retuned without touching the script.
@property is the detail that makes it feel good. Registering the two rotations as typed <angle> values lets the browser interpolate them, so releasing the card settles rather than snaps. Registration has no utility form and lives in the stylesheet.
It refuses to run where it would be wrong. (hover:none) and (prefers-reduced-motion:reduce) both return early, and the matching motion-reduce: utilities flatten the card so nothing is left half-transformed.
Make it yours
- Tilt intensity: the
MAXconstant in the script — ±8° is subtle, ±18° dramatic. - Parallax depth: the caption's
translateZ(50px). - Glare: restyle or delete the
after:layer for a matte tilt.
Gotchas — read before shipping
- Write variables, not transform strings. Setting
style.transformfrom JS takes ownership of the property and CSS can no longer ease it. - The card needs
transform-3d(transform-style: preserve-3d) or the caption'stranslateZflattens to nothing. - Guard for touch. A tilt that only responds to a cursor must be inert, not broken, where there is no cursor.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
@property gates the eased settle (Chrome 85+, Safari 16.4+, Firefox 128+); without it the tilt still tracks the pointer, just without the smooth return.