Pure TailwindMIT licensed

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.

Live Demo
Plain CSSTailwind v4
Try it

Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time

Untitled UI React · Affiliate link

Get Untitled UI Pro

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>
@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.

1Load Tailwind
<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.

2Add the stylesheetRequiredthe @theme block is what makes the custom utilities exist
@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;
}
3Fonts used by this demooptional — skip if you have your own type stack
<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 @theme animation entry and one w-max replace 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.
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
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 &minus;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 @theme animation 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 &minus;50% of the doubled track. Any mismatch and the loop visibly jumps once per cycle.
  • w-max is 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

ChromeSafariFirefoxEdge
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.

Techniques used in this demo

Utilitiesfirst-class utility or variant

Arbitraryneeds bracket syntax

Plain CSSno Tailwind surface

Search CodeFronts

Loading…