06 / 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 Interactive Pointer Trail Image Gallery
A canvas where photographs bloom behind the cursor, drift and fade. CSS owns every frame of the motion; twelve lines of JavaScript decide only where and when to spawn. In Tailwind the spawned node's entire look is one class string the script assigns, which is the cleanest possible split between behaviour and style.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="tgl-06 w-full min-h-svh grid place-items-center px-5 py-10 font-tgl06-sans bg-[radial-gradient(120%_120%_at_50%_30%,oklch(0.22_0.04_300),oklch(0.09_0.02_285))]" aria-label="Interactive pointer trail gallery">
<div id="tgl-06-canvas" role="img" aria-label="Move your cursor to leave a trail of images" class="relative w-[min(860px,100%)] h-[min(520px,70vh)] rounded-[20px] overflow-hidden cursor-crosshair touch-none border border-white/10 bg-[linear-gradient(135deg,rgba(255,255,255,0.03),rgba(255,255,255,0.01))]">
<p class="absolute inset-0 grid place-items-center m-0 text-white/40 font-tgl06-mono text-xs tracking-[0.2em] uppercase pointer-events-none">move your cursor across the canvas</p>
</div>
</section><section class="tgl-06 w-full min-h-svh grid place-items-center px-5 py-10 font-tgl06-sans bg-[radial-gradient(120%_120%_at_50%_30%,oklch(0.22_0.04_300),oklch(0.09_0.02_285))]" aria-label="Interactive pointer trail gallery">
<div id="tgl-06-canvas" role="img" aria-label="Move your cursor to leave a trail of images" class="relative w-[min(860px,100%)] h-[min(520px,70vh)] rounded-[20px] overflow-hidden cursor-crosshair touch-none border border-white/10 bg-[linear-gradient(135deg,rgba(255,255,255,0.03),rgba(255,255,255,0.01))]">
<p class="absolute inset-0 grid place-items-center m-0 text-white/40 font-tgl06-mono text-xs tracking-[0.2em] uppercase pointer-events-none">move your cursor across the canvas</p>
</div>
</section>@import "tailwindcss";
@theme {
--color-tgl06-acc: #d982e8;
--color-tgl06-acc: oklch(0.74 0.17 320);
--font-tgl06-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tgl06-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
--animate-tgl06-bloom: tgl06-bloom 1.1s cubic-bezier(0.2, 0.7, 0.2, 1) forwards;
}
@keyframes tgl06-bloom {
0% {
opacity: 0;
transform: scale(0.4) rotate(-6deg);
}
18% {
opacity: 1;
transform: scale(1) rotate(0deg);
}
100% {
opacity: 0;
transform: scale(0.86) translateY(40px) rotate(4deg);
}
}
.tgl-06 {
width: 100%;
min-height: 100svh;
}
.tgl-06 ::selection {
background: var(--color-tgl06-acc);
color: #fff;
}@import "tailwindcss";
@theme {
--color-tgl06-acc: #d982e8;
--color-tgl06-acc: oklch(0.74 0.17 320);
--font-tgl06-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tgl06-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
--animate-tgl06-bloom: tgl06-bloom 1.1s cubic-bezier(0.2, 0.7, 0.2, 1) forwards;
}
@keyframes tgl06-bloom {
0% {
opacity: 0;
transform: scale(0.4) rotate(-6deg);
}
18% {
opacity: 1;
transform: scale(1) rotate(0deg);
}
100% {
opacity: 0;
transform: scale(0.86) translateY(40px) rotate(4deg);
}
}
.tgl-06 {
width: 100%;
min-height: 100svh;
}
.tgl-06 ::selection {
background: var(--color-tgl06-acc);
color: #fff;
}(function () {
var canvas = document.getElementById('tgl-06-canvas');
if (!canvas) return;
if (matchMedia('(hover:none)').matches || matchMedia('(prefers-reduced-motion:reduce)').matches) return;
// The spawned node's entire appearance, as Tailwind utilities.
var SPARK = 'absolute w-[120px] h-[150px] -mt-[75px] -ml-[60px] rounded-xl object-cover ' +
'pointer-events-none will-change-[transform,opacity] ' +
'shadow-[0_12px_30px_-12px_rgba(0,0,0,0.6)] animate-tgl06-bloom';
var pool = [1801, 1803, 1002, 1601, 1101, 1301].map(function (n) {
return 'https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?q=80&w=240&h=300&auto=format&fit=crop?lock=' + n;
});
var i = 0, last = 0;
canvas.addEventListener('pointermove', function (e) {
var now = Date.now();
if (now - last < 90) return; // throttle spawn rate
last = now;
var r = canvas.getBoundingClientRect();
var img = document.createElement('img');
img.className = SPARK;
img.alt = '';
img.setAttribute('aria-hidden', 'true');
img.src = pool[i % pool.length]; i++;
img.style.left = (e.clientX - r.left) + 'px';
img.style.top = (e.clientY - r.top) + 'px';
img.addEventListener('animationend', function () { img.remove(); }, { once: true });
canvas.appendChild(img);
});
})();(function () {
var canvas = document.getElementById('tgl-06-canvas');
if (!canvas) return;
if (matchMedia('(hover:none)').matches || matchMedia('(prefers-reduced-motion:reduce)').matches) return;
// The spawned node's entire appearance, as Tailwind utilities.
var SPARK = 'absolute w-[120px] h-[150px] -mt-[75px] -ml-[60px] rounded-xl object-cover ' +
'pointer-events-none will-change-[transform,opacity] ' +
'shadow-[0_12px_30px_-12px_rgba(0,0,0,0.6)] animate-tgl06-bloom';
var pool = [1801, 1803, 1002, 1601, 1101, 1301].map(function (n) {
return 'https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?q=80&w=240&h=300&auto=format&fit=crop?lock=' + n;
});
var i = 0, last = 0;
canvas.addEventListener('pointermove', function (e) {
var now = Date.now();
if (now - last < 90) return; // throttle spawn rate
last = now;
var r = canvas.getBoundingClientRect();
var img = document.createElement('img');
img.className = SPARK;
img.alt = '';
img.setAttribute('aria-hidden', 'true');
img.src = pool[i % pool.length]; i++;
img.style.left = (e.clientX - r.left) + 'px';
img.style.top = (e.clientY - r.top) + 'px';
img.addEventListener('animationend', function () { img.remove(); }, { once: true });
canvas.appendChild(img);
});
})();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-tgl06-acc: #d982e8;
--color-tgl06-acc: oklch(0.74 0.17 320);
--font-tgl06-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tgl06-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
--animate-tgl06-bloom: tgl06-bloom 1.1s cubic-bezier(0.2, 0.7, 0.2, 1) forwards;
}
@keyframes tgl06-bloom {
0% {
opacity: 0;
transform: scale(0.4) rotate(-6deg);
}
18% {
opacity: 1;
transform: scale(1) rotate(0deg);
}
100% {
opacity: 0;
transform: scale(0.86) translateY(40px) rotate(4deg);
}
}
.tgl-06 {
width: 100%;
min-height: 100svh;
}
.tgl-06 ::selection {
background: var(--color-tgl06-acc);
color: #fff;
}@import "tailwindcss";
@theme {
--color-tgl06-acc: #d982e8;
--color-tgl06-acc: oklch(0.74 0.17 320);
--font-tgl06-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tgl06-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
--animate-tgl06-bloom: tgl06-bloom 1.1s cubic-bezier(0.2, 0.7, 0.2, 1) forwards;
}
@keyframes tgl06-bloom {
0% {
opacity: 0;
transform: scale(0.4) rotate(-6deg);
}
18% {
opacity: 1;
transform: scale(1) rotate(0deg);
}
100% {
opacity: 0;
transform: scale(0.86) translateY(40px) rotate(4deg);
}
}
.tgl-06 {
width: 100%;
min-height: 100svh;
}
.tgl-06 ::selection {
background: var(--color-tgl06-acc);
color: #fff;
}<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;600&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;600&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- The spawned element has no stylesheet class at all: its look is a utility string in the script, which means the visual and the code that creates it move together. This is the one demo where putting styles in JavaScript is the right answer.
--animate-tgl06-bloomas a@themeentry keeps the timing function and duration in the stylesheet where a designer can retune them, while the script stays purely about position.- UI change from the CSS original: the
animationendlistener is registered with{ once: true }, so the node cannot leak a listener if an animation restarts — a small correctness fix on a hot path.
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 Interactive Pointer Trail Image Gallery
Source: https://codefronts.com/snippets/tailwind-css-image-gallery/tailwind-interactive-pointer-trail-image-gallery/
A canvas where photographs bloom behind the cursor, drift and fade. CSS owns every frame of the motion; twelve lines of JavaScript decide only where and when to spawn. In Tailwind the spawned node's entire look is one class string the script assigns, which is the cleanest possible split between behaviour and style.
## HTML
```html
<section class="tgl-06 w-full min-h-svh grid place-items-center px-5 py-10 font-tgl06-sans bg-[radial-gradient(120%_120%_at_50%_30%,oklch(0.22_0.04_300),oklch(0.09_0.02_285))]" aria-label="Interactive pointer trail gallery">
<div id="tgl-06-canvas" role="img" aria-label="Move your cursor to leave a trail of images" class="relative w-[min(860px,100%)] h-[min(520px,70vh)] rounded-[20px] overflow-hidden cursor-crosshair touch-none border border-white/10 bg-[linear-gradient(135deg,rgba(255,255,255,0.03),rgba(255,255,255,0.01))]">
<p class="absolute inset-0 grid place-items-center m-0 text-white/40 font-tgl06-mono text-xs tracking-[0.2em] uppercase pointer-events-none">move your cursor across the canvas</p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tgl06-acc: #d982e8;
--color-tgl06-acc: oklch(0.74 0.17 320);
--font-tgl06-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tgl06-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
--animate-tgl06-bloom: tgl06-bloom 1.1s cubic-bezier(0.2, 0.7, 0.2, 1) forwards;
}
@keyframes tgl06-bloom {
0% {
opacity: 0;
transform: scale(0.4) rotate(-6deg);
}
18% {
opacity: 1;
transform: scale(1) rotate(0deg);
}
100% {
opacity: 0;
transform: scale(0.86) translateY(40px) rotate(4deg);
}
}
.tgl-06 {
width: 100%;
min-height: 100svh;
}
.tgl-06 ::selection {
background: var(--color-tgl06-acc);
color: #fff;
}
```
## JavaScript
```js
(function () {
var canvas = document.getElementById('tgl-06-canvas');
if (!canvas) return;
if (matchMedia('(hover:none)').matches || matchMedia('(prefers-reduced-motion:reduce)').matches) return;
// The spawned node's entire appearance, as Tailwind utilities.
var SPARK = 'absolute w-[120px] h-[150px] -mt-[75px] -ml-[60px] rounded-xl object-cover ' +
'pointer-events-none will-change-[transform,opacity] ' +
'shadow-[0_12px_30px_-12px_rgba(0,0,0,0.6)] animate-tgl06-bloom';
var pool = [1801, 1803, 1002, 1601, 1101, 1301].map(function (n) {
return 'https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?q=80&w=240&h=300&auto=format&fit=crop?lock=' + n;
});
var i = 0, last = 0;
canvas.addEventListener('pointermove', function (e) {
var now = Date.now();
if (now - last < 90) return; // throttle spawn rate
last = now;
var r = canvas.getBoundingClientRect();
var img = document.createElement('img');
img.className = SPARK;
img.alt = '';
img.setAttribute('aria-hidden', 'true');
img.src = pool[i % pool.length]; i++;
img.style.left = (e.clientX - r.left) + 'px';
img.style.top = (e.clientY - r.top) + 'px';
img.addEventListener('animationend', function () { img.remove(); }, { once: true });
canvas.appendChild(img);
});
})();
```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 Interactive Pointer Trail Image Gallery
Source: https://codefronts.com/snippets/tailwind-css-image-gallery/tailwind-interactive-pointer-trail-image-gallery/
A canvas where photographs bloom behind the cursor, drift and fade. CSS owns every frame of the motion; twelve lines of JavaScript decide only where and when to spawn. In Tailwind the spawned node's entire look is one class string the script assigns, which is the cleanest possible split between behaviour and style.
## HTML
```html
<section class="tgl-06 w-full min-h-svh grid place-items-center px-5 py-10 font-tgl06-sans bg-[radial-gradient(120%_120%_at_50%_30%,oklch(0.22_0.04_300),oklch(0.09_0.02_285))]" aria-label="Interactive pointer trail gallery">
<div id="tgl-06-canvas" role="img" aria-label="Move your cursor to leave a trail of images" class="relative w-[min(860px,100%)] h-[min(520px,70vh)] rounded-[20px] overflow-hidden cursor-crosshair touch-none border border-white/10 bg-[linear-gradient(135deg,rgba(255,255,255,0.03),rgba(255,255,255,0.01))]">
<p class="absolute inset-0 grid place-items-center m-0 text-white/40 font-tgl06-mono text-xs tracking-[0.2em] uppercase pointer-events-none">move your cursor across the canvas</p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tgl06-acc: #d982e8;
--color-tgl06-acc: oklch(0.74 0.17 320);
--font-tgl06-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tgl06-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
--animate-tgl06-bloom: tgl06-bloom 1.1s cubic-bezier(0.2, 0.7, 0.2, 1) forwards;
}
@keyframes tgl06-bloom {
0% {
opacity: 0;
transform: scale(0.4) rotate(-6deg);
}
18% {
opacity: 1;
transform: scale(1) rotate(0deg);
}
100% {
opacity: 0;
transform: scale(0.86) translateY(40px) rotate(4deg);
}
}
.tgl-06 {
width: 100%;
min-height: 100svh;
}
.tgl-06 ::selection {
background: var(--color-tgl06-acc);
color: #fff;
}
```
## JavaScript
```js
(function () {
var canvas = document.getElementById('tgl-06-canvas');
if (!canvas) return;
if (matchMedia('(hover:none)').matches || matchMedia('(prefers-reduced-motion:reduce)').matches) return;
// The spawned node's entire appearance, as Tailwind utilities.
var SPARK = 'absolute w-[120px] h-[150px] -mt-[75px] -ml-[60px] rounded-xl object-cover ' +
'pointer-events-none will-change-[transform,opacity] ' +
'shadow-[0_12px_30px_-12px_rgba(0,0,0,0.6)] animate-tgl06-bloom';
var pool = [1801, 1803, 1002, 1601, 1101, 1301].map(function (n) {
return 'https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?q=80&w=240&h=300&auto=format&fit=crop?lock=' + n;
});
var i = 0, last = 0;
canvas.addEventListener('pointermove', function (e) {
var now = Date.now();
if (now - last < 90) return; // throttle spawn rate
last = now;
var r = canvas.getBoundingClientRect();
var img = document.createElement('img');
img.className = SPARK;
img.alt = '';
img.setAttribute('aria-hidden', 'true');
img.src = pool[i % pool.length]; i++;
img.style.left = (e.clientX - r.left) + 'px';
img.style.top = (e.clientY - r.top) + 'px';
img.addEventListener('animationend', function () { img.remove(); }, { once: true });
canvas.appendChild(img);
});
})();
```How this works
JS supplies coordinates; CSS supplies the animation. On each throttled pointermove the handler creates an <img>, positions it, hands it the spark class list and appends it. The bloom — scale up, drift down, fade out — is a single keyframe minted as a utility:
--animate-tgl06-bloom: tgl06-bloom 1.1s cubic-bezier(.2,.7,.2,1) forwards;The node cleans up after itself. A one-shot animationend listener removes it, so the canvas never accumulates nodes and INP stays flat.
It refuses to run where it makes no sense. No cursor, no trail; reduced motion, no trail. In both cases the canvas keeps its hint text and reads as a deliberate empty stage.
Make it yours
- Trail density: the 90ms throttle in the handler. Lower spawns more.
- Imagery: the URL pool at the top of the script.
- Bloom character: edit the
@themeanimation entry — spin, blur-in, longer drift — without touching the script.
Gotchas — read before shipping
- Throttle and self-remove, always. An unthrottled spawner adds hundreds of nodes per second and the page stops responding.
- Spawned nodes carry
alt=""andaria-hidden="true"— they are pure decoration and must not reach the accessibility tree. - The class string lives in JS, so a Tailwind CLI build needs the script file in its
@sourcescan or those utilities get tree-shaken out.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
Keyframes and Pointer Events are universal; the floor is Tailwind v4's generated sheet. With JS disabled the canvas renders as a static panel.