19 / 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 Infinite Scrolling Image Marquee with Seamless Loop
A banner of images that scrolls forever with no visible seam or reset, pausing when you hover it. The loop is the oldest trick in the book — duplicate the set, translate −50% — and Tailwind turns it into one @theme animation plus one mask.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="tgl-19 w-full min-h-svh grid place-items-center bg-tgl19-bg font-tgl19-sans py-10" aria-label="Infinite scrolling image strip">
<div class="group w-full overflow-hidden [mask-image:linear-gradient(90deg,transparent,#000_12%,#000_88%,transparent)]">
<div class="flex gap-4 w-max animate-tgl19-marquee group-hover:[animation-play-state:paused] motion-reduce:animate-none">
<img src="https://images.unsplash.com/photo-1449157291145-7efd050a4d0e?q=80&w=300&h=300&auto=format&fit=crop" alt="Concrete facade in monochrome" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05] transition-[filter] duration-[400ms] hover:grayscale-0 motion-reduce:transition-none">
<img src="https://images.unsplash.com/photo-1485846234645-a62644f84728?q=80&w=300&h=300&auto=format&fit=crop" alt="City block from above" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05] transition-[filter] duration-[400ms] hover:grayscale-0 motion-reduce:transition-none">
<img src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?q=80&w=300&h=300&auto=format&fit=crop" alt="Bare trees in winter" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05] transition-[filter] duration-[400ms] hover:grayscale-0 motion-reduce:transition-none">
<img src="https://images.unsplash.com/photo-1633356122544-f134324a6cee?q=80&w=300&h=300&auto=format&fit=crop" alt="Portrait in hard light" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05] transition-[filter] duration-[400ms] hover:grayscale-0 motion-reduce:transition-none">
<img src="https://images.unsplash.com/photo-1531891437562-4301cf35b7e4?q=80&w=300&h=300&auto=format&fit=crop" alt="Crossing in the rain" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05] transition-[filter] duration-[400ms] hover:grayscale-0 motion-reduce:transition-none">
<img src="https://images.unsplash.com/photo-1504674900247-0877df9cc836?q=80&w=300&h=300&auto=format&fit=crop" alt="Ridge line against cloud" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05] transition-[filter] duration-[400ms] hover:grayscale-0 motion-reduce:transition-none">
<img src="https://images.unsplash.com/photo-1449157291145-7efd050a4d0e?q=80&w=300&h=300&auto=format&fit=crop" alt="" aria-hidden="true" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05]">
<img src="https://images.unsplash.com/photo-1485846234645-a62644f84728?q=80&w=300&h=300&auto=format&fit=crop" alt="" aria-hidden="true" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05]">
<img src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?q=80&w=300&h=300&auto=format&fit=crop" alt="" aria-hidden="true" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05]">
<img src="https://images.unsplash.com/photo-1633356122544-f134324a6cee?q=80&w=300&h=300&auto=format&fit=crop" alt="" aria-hidden="true" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05]">
<img src="https://images.unsplash.com/photo-1531891437562-4301cf35b7e4?q=80&w=300&h=300&auto=format&fit=crop" alt="" aria-hidden="true" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05]">
<img src="https://images.unsplash.com/photo-1504674900247-0877df9cc836?q=80&w=300&h=300&auto=format&fit=crop" alt="" aria-hidden="true" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05]">
</div>
</div>
</section><section class="tgl-19 w-full min-h-svh grid place-items-center bg-tgl19-bg font-tgl19-sans py-10" aria-label="Infinite scrolling image strip">
<div class="group w-full overflow-hidden [mask-image:linear-gradient(90deg,transparent,#000_12%,#000_88%,transparent)]">
<div class="flex gap-4 w-max animate-tgl19-marquee group-hover:[animation-play-state:paused] motion-reduce:animate-none">
<img src="https://images.unsplash.com/photo-1449157291145-7efd050a4d0e?q=80&w=300&h=300&auto=format&fit=crop" alt="Concrete facade in monochrome" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05] transition-[filter] duration-[400ms] hover:grayscale-0 motion-reduce:transition-none">
<img src="https://images.unsplash.com/photo-1485846234645-a62644f84728?q=80&w=300&h=300&auto=format&fit=crop" alt="City block from above" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05] transition-[filter] duration-[400ms] hover:grayscale-0 motion-reduce:transition-none">
<img src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?q=80&w=300&h=300&auto=format&fit=crop" alt="Bare trees in winter" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05] transition-[filter] duration-[400ms] hover:grayscale-0 motion-reduce:transition-none">
<img src="https://images.unsplash.com/photo-1633356122544-f134324a6cee?q=80&w=300&h=300&auto=format&fit=crop" alt="Portrait in hard light" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05] transition-[filter] duration-[400ms] hover:grayscale-0 motion-reduce:transition-none">
<img src="https://images.unsplash.com/photo-1531891437562-4301cf35b7e4?q=80&w=300&h=300&auto=format&fit=crop" alt="Crossing in the rain" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05] transition-[filter] duration-[400ms] hover:grayscale-0 motion-reduce:transition-none">
<img src="https://images.unsplash.com/photo-1504674900247-0877df9cc836?q=80&w=300&h=300&auto=format&fit=crop" alt="Ridge line against cloud" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05] transition-[filter] duration-[400ms] hover:grayscale-0 motion-reduce:transition-none">
<img src="https://images.unsplash.com/photo-1449157291145-7efd050a4d0e?q=80&w=300&h=300&auto=format&fit=crop" alt="" aria-hidden="true" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05]">
<img src="https://images.unsplash.com/photo-1485846234645-a62644f84728?q=80&w=300&h=300&auto=format&fit=crop" alt="" aria-hidden="true" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05]">
<img src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?q=80&w=300&h=300&auto=format&fit=crop" alt="" aria-hidden="true" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05]">
<img src="https://images.unsplash.com/photo-1633356122544-f134324a6cee?q=80&w=300&h=300&auto=format&fit=crop" alt="" aria-hidden="true" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05]">
<img src="https://images.unsplash.com/photo-1531891437562-4301cf35b7e4?q=80&w=300&h=300&auto=format&fit=crop" alt="" aria-hidden="true" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05]">
<img src="https://images.unsplash.com/photo-1504674900247-0877df9cc836?q=80&w=300&h=300&auto=format&fit=crop" alt="" aria-hidden="true" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05]">
</div>
</div>
</section>@import "tailwindcss";
@theme {
--color-tgl19-bg: #0f0f0f;
--color-tgl19-bg: oklch(0.16 0 0);
--color-tgl19-acc: #e08a4a;
--color-tgl19-acc: oklch(0.72 0.13 50);
--font-tgl19-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-tgl19-marquee: tgl19-marquee 30s linear infinite;
}
@keyframes tgl19-marquee {
to {
transform: translateX(-50%);
}
}
.tgl-19 {
width: 100%;
min-height: 100svh;
}
/* Older WebKit mask prefix. */
.tgl-19 > div {
-webkit-mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
}
.tgl-19 ::selection {
background: var(--color-tgl19-acc);
color: #111;
}@import "tailwindcss";
@theme {
--color-tgl19-bg: #0f0f0f;
--color-tgl19-bg: oklch(0.16 0 0);
--color-tgl19-acc: #e08a4a;
--color-tgl19-acc: oklch(0.72 0.13 50);
--font-tgl19-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-tgl19-marquee: tgl19-marquee 30s linear infinite;
}
@keyframes tgl19-marquee {
to {
transform: translateX(-50%);
}
}
.tgl-19 {
width: 100%;
min-height: 100svh;
}
/* Older WebKit mask prefix. */
.tgl-19 > div {
-webkit-mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
}
.tgl-19 ::selection {
background: var(--color-tgl19-acc);
color: #111;
}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-tgl19-bg: #0f0f0f;
--color-tgl19-bg: oklch(0.16 0 0);
--color-tgl19-acc: #e08a4a;
--color-tgl19-acc: oklch(0.72 0.13 50);
--font-tgl19-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-tgl19-marquee: tgl19-marquee 30s linear infinite;
}
@keyframes tgl19-marquee {
to {
transform: translateX(-50%);
}
}
.tgl-19 {
width: 100%;
min-height: 100svh;
}
/* Older WebKit mask prefix. */
.tgl-19 > div {
-webkit-mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
}
.tgl-19 ::selection {
background: var(--color-tgl19-acc);
color: #111;
}@import "tailwindcss";
@theme {
--color-tgl19-bg: #0f0f0f;
--color-tgl19-bg: oklch(0.16 0 0);
--color-tgl19-acc: #e08a4a;
--color-tgl19-acc: oklch(0.72 0.13 50);
--font-tgl19-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-tgl19-marquee: tgl19-marquee 30s linear infinite;
}
@keyframes tgl19-marquee {
to {
transform: translateX(-50%);
}
}
.tgl-19 {
width: 100%;
min-height: 100svh;
}
/* Older WebKit mask prefix. */
.tgl-19 > div {
-webkit-mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
}
.tgl-19 ::selection {
background: var(--color-tgl19-acc);
color: #111;
}<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&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&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- One
@themeanimation entry and onew-maxreplace the whole marquee stylesheet. The mask stays arbitrary because an edge-fade gradient is composition, not a token. - Pausing is
group-hover:on the track rather than a descendant rule on the viewport, so the pause and the animation sit on the same element. - UI change from the CSS original: the duplicated copies no longer carry the hover transition — only the six real images do. Hovering a decorative duplicate used to restore its colour, which broke the illusion that the strip is one continuous set.
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 Infinite Scrolling Image Marquee with Seamless Loop
Source: https://codefronts.com/snippets/tailwind-css-image-gallery/tailwind-infinite-scrolling-image-marquee/
A banner of images that scrolls forever with no visible seam or reset, pausing when you hover it. The loop is the oldest trick in the book — duplicate the set, translate −50% — and Tailwind turns it into one @theme animation plus one mask.
## HTML
```html
<section class="tgl-19 w-full min-h-svh grid place-items-center bg-tgl19-bg font-tgl19-sans py-10" aria-label="Infinite scrolling image strip">
<div class="group w-full overflow-hidden [mask-image:linear-gradient(90deg,transparent,#000_12%,#000_88%,transparent)]">
<div class="flex gap-4 w-max animate-tgl19-marquee group-hover:[animation-play-state:paused] motion-reduce:animate-none">
<img src="https://images.unsplash.com/photo-1449157291145-7efd050a4d0e?q=80&w=300&h=300&auto=format&fit=crop" alt="Concrete facade in monochrome" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05] transition-[filter] duration-[400ms] hover:grayscale-0 motion-reduce:transition-none">
<img src="https://images.unsplash.com/photo-1485846234645-a62644f84728?q=80&w=300&h=300&auto=format&fit=crop" alt="City block from above" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05] transition-[filter] duration-[400ms] hover:grayscale-0 motion-reduce:transition-none">
<img src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?q=80&w=300&h=300&auto=format&fit=crop" alt="Bare trees in winter" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05] transition-[filter] duration-[400ms] hover:grayscale-0 motion-reduce:transition-none">
<img src="https://images.unsplash.com/photo-1633356122544-f134324a6cee?q=80&w=300&h=300&auto=format&fit=crop" alt="Portrait in hard light" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05] transition-[filter] duration-[400ms] hover:grayscale-0 motion-reduce:transition-none">
<img src="https://images.unsplash.com/photo-1531891437562-4301cf35b7e4?q=80&w=300&h=300&auto=format&fit=crop" alt="Crossing in the rain" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05] transition-[filter] duration-[400ms] hover:grayscale-0 motion-reduce:transition-none">
<img src="https://images.unsplash.com/photo-1504674900247-0877df9cc836?q=80&w=300&h=300&auto=format&fit=crop" alt="Ridge line against cloud" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05] transition-[filter] duration-[400ms] hover:grayscale-0 motion-reduce:transition-none">
<img src="https://images.unsplash.com/photo-1449157291145-7efd050a4d0e?q=80&w=300&h=300&auto=format&fit=crop" alt="" aria-hidden="true" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05]">
<img src="https://images.unsplash.com/photo-1485846234645-a62644f84728?q=80&w=300&h=300&auto=format&fit=crop" alt="" aria-hidden="true" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05]">
<img src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?q=80&w=300&h=300&auto=format&fit=crop" alt="" aria-hidden="true" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05]">
<img src="https://images.unsplash.com/photo-1633356122544-f134324a6cee?q=80&w=300&h=300&auto=format&fit=crop" alt="" aria-hidden="true" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05]">
<img src="https://images.unsplash.com/photo-1531891437562-4301cf35b7e4?q=80&w=300&h=300&auto=format&fit=crop" alt="" aria-hidden="true" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05]">
<img src="https://images.unsplash.com/photo-1504674900247-0877df9cc836?q=80&w=300&h=300&auto=format&fit=crop" alt="" aria-hidden="true" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05]">
</div>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tgl19-bg: #0f0f0f;
--color-tgl19-bg: oklch(0.16 0 0);
--color-tgl19-acc: #e08a4a;
--color-tgl19-acc: oklch(0.72 0.13 50);
--font-tgl19-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-tgl19-marquee: tgl19-marquee 30s linear infinite;
}
@keyframes tgl19-marquee {
to {
transform: translateX(-50%);
}
}
.tgl-19 {
width: 100%;
min-height: 100svh;
}
/* Older WebKit mask prefix. */
.tgl-19 > div {
-webkit-mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
}
.tgl-19 ::selection {
background: var(--color-tgl19-acc);
color: #111;
}
```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 Infinite Scrolling Image Marquee with Seamless Loop
Source: https://codefronts.com/snippets/tailwind-css-image-gallery/tailwind-infinite-scrolling-image-marquee/
A banner of images that scrolls forever with no visible seam or reset, pausing when you hover it. The loop is the oldest trick in the book — duplicate the set, translate −50% — and Tailwind turns it into one @theme animation plus one mask.
## HTML
```html
<section class="tgl-19 w-full min-h-svh grid place-items-center bg-tgl19-bg font-tgl19-sans py-10" aria-label="Infinite scrolling image strip">
<div class="group w-full overflow-hidden [mask-image:linear-gradient(90deg,transparent,#000_12%,#000_88%,transparent)]">
<div class="flex gap-4 w-max animate-tgl19-marquee group-hover:[animation-play-state:paused] motion-reduce:animate-none">
<img src="https://images.unsplash.com/photo-1449157291145-7efd050a4d0e?q=80&w=300&h=300&auto=format&fit=crop" alt="Concrete facade in monochrome" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05] transition-[filter] duration-[400ms] hover:grayscale-0 motion-reduce:transition-none">
<img src="https://images.unsplash.com/photo-1485846234645-a62644f84728?q=80&w=300&h=300&auto=format&fit=crop" alt="City block from above" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05] transition-[filter] duration-[400ms] hover:grayscale-0 motion-reduce:transition-none">
<img src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?q=80&w=300&h=300&auto=format&fit=crop" alt="Bare trees in winter" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05] transition-[filter] duration-[400ms] hover:grayscale-0 motion-reduce:transition-none">
<img src="https://images.unsplash.com/photo-1633356122544-f134324a6cee?q=80&w=300&h=300&auto=format&fit=crop" alt="Portrait in hard light" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05] transition-[filter] duration-[400ms] hover:grayscale-0 motion-reduce:transition-none">
<img src="https://images.unsplash.com/photo-1531891437562-4301cf35b7e4?q=80&w=300&h=300&auto=format&fit=crop" alt="Crossing in the rain" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05] transition-[filter] duration-[400ms] hover:grayscale-0 motion-reduce:transition-none">
<img src="https://images.unsplash.com/photo-1504674900247-0877df9cc836?q=80&w=300&h=300&auto=format&fit=crop" alt="Ridge line against cloud" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05] transition-[filter] duration-[400ms] hover:grayscale-0 motion-reduce:transition-none">
<img src="https://images.unsplash.com/photo-1449157291145-7efd050a4d0e?q=80&w=300&h=300&auto=format&fit=crop" alt="" aria-hidden="true" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05]">
<img src="https://images.unsplash.com/photo-1485846234645-a62644f84728?q=80&w=300&h=300&auto=format&fit=crop" alt="" aria-hidden="true" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05]">
<img src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?q=80&w=300&h=300&auto=format&fit=crop" alt="" aria-hidden="true" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05]">
<img src="https://images.unsplash.com/photo-1633356122544-f134324a6cee?q=80&w=300&h=300&auto=format&fit=crop" alt="" aria-hidden="true" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05]">
<img src="https://images.unsplash.com/photo-1531891437562-4301cf35b7e4?q=80&w=300&h=300&auto=format&fit=crop" alt="" aria-hidden="true" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05]">
<img src="https://images.unsplash.com/photo-1504674900247-0877df9cc836?q=80&w=300&h=300&auto=format&fit=crop" alt="" aria-hidden="true" loading="lazy" decoding="async" class="block w-[220px] h-[220px] flex-[0_0_auto] object-cover rounded-xl grayscale contrast-[1.05]">
</div>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tgl19-bg: #0f0f0f;
--color-tgl19-bg: oklch(0.16 0 0);
--color-tgl19-acc: #e08a4a;
--color-tgl19-acc: oklch(0.72 0.13 50);
--font-tgl19-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-tgl19-marquee: tgl19-marquee 30s linear infinite;
}
@keyframes tgl19-marquee {
to {
transform: translateX(-50%);
}
}
.tgl-19 {
width: 100%;
min-height: 100svh;
}
/* Older WebKit mask prefix. */
.tgl-19 > div {
-webkit-mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
}
.tgl-19 ::selection {
background: var(--color-tgl19-acc);
color: #111;
}
```How this works
The seam does not exist because the content repeats. The track holds the image set twice; the animation translates it −50%, which is exactly one copy. At the end of a cycle the strip is pixel-identical to its start, so the reset is invisible.
The duplicate is invisible to assistive tech. Every copied image carries alt="" and aria-hidden="true", so a screen reader announces six photographs, not twelve.
Reduced motion stops it dead. motion-reduce:animate-none leaves a static row of images — the required behaviour for auto-moving content, and one utility.
Make it yours
- Speed: the duration in the
@themeanimation entry. - Direction:
[animation-direction:reverse], or animate to+50%. - Two rows moving opposite ways make a woven wall — copy the strip and reverse the second.
Gotchas — read before shipping
- The duplicate must be exact and the translate must be −50% of the doubled track. Any mismatch and the loop visibly jumps once per cycle.
w-maxis required on the track; without it the flex row wraps and the whole illusion collapses.- Auto-motion without a reduced-motion stop is an accessibility failure, not a preference — this is the demo where
motion-reduce:is non-negotiable.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
Keyframes and mask-image are Baseline; the floor is Tailwind v4's generated sheet. Older WebKit needs the -webkit-mask-image prefix, included in the stylesheet.