22 CSS Image Galleries19 / 22

Pure CSSMIT licensed

CSS Infinite Scroll Image Strip

A seamless, endlessly looping horizontal banner of images that scrolls forever with no visible seam or reset — the marquee/logo-wall effect brand pages and 'as featured in' rows search for. Pure CSS keyframes.

Published Updated

Live Demo
Try it

The code

<section class="gl-18" aria-label="Infinite scrolling image strip">
  <div class="gl-18__viewport">
    <div class="gl-18__track">
      <img src="https://loremflickr.com/300/300/architecture,mono?lock=1801" alt="Gallery image 1" loading="lazy" decoding="async">
      <img src="https://loremflickr.com/300/300/city,mono?lock=1802" alt="Gallery image 2" loading="lazy" decoding="async">
      <img src="https://loremflickr.com/300/300/nature,mono?lock=1803" alt="Gallery image 3" loading="lazy" decoding="async">
      <img src="https://loremflickr.com/300/300/portrait,mono?lock=1804" alt="Gallery image 4" loading="lazy" decoding="async">
      <img src="https://loremflickr.com/300/300/street,mono?lock=1805" alt="Gallery image 5" loading="lazy" decoding="async">
      <img src="https://loremflickr.com/300/300/mountain,mono?lock=1806" alt="Gallery image 6" loading="lazy" decoding="async">
      <img src="https://loremflickr.com/300/300/architecture,mono?lock=1801" alt="" aria-hidden="true" loading="lazy" decoding="async">
      <img src="https://loremflickr.com/300/300/city,mono?lock=1802" alt="" aria-hidden="true" loading="lazy" decoding="async">
      <img src="https://loremflickr.com/300/300/nature,mono?lock=1803" alt="" aria-hidden="true" loading="lazy" decoding="async">
      <img src="https://loremflickr.com/300/300/portrait,mono?lock=1804" alt="" aria-hidden="true" loading="lazy" decoding="async">
      <img src="https://loremflickr.com/300/300/street,mono?lock=1805" alt="" aria-hidden="true" loading="lazy" decoding="async">
      <img src="https://loremflickr.com/300/300/mountain,mono?lock=1806" alt="" aria-hidden="true" loading="lazy" decoding="async">
    </div>
  </div>
</section>
.gl-18,
.gl-18 *,
.gl-18 *::before,
.gl-18 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.gl-18 ::selection {
  background: oklch(0.7 0.14 30);
  color: #111;
}

.gl-18 {
  font-family: 'Segoe UI',system-ui,sans-serif;
  width: 100%;
  min-height: 100vh;
  padding: 40px 0;
  background: #0f0f0f;
  display: flex;
  align-items: center;
  justify-content: center;
}

.gl-18__viewport {
  width: 100%;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(90deg,transparent,#000 12%,#000 88%,transparent);
  mask-image: linear-gradient(90deg,transparent,#000 12%,#000 88%,transparent);
}

.gl-18__track {
  display: flex;
  gap: 16px;
  width: max-content;
  animation: gl-18-marquee 30s linear infinite;
}

.gl-18__viewport:hover .gl-18__track {
  animation-play-state: paused;
}

.gl-18__track img {
  display: block;
  width: 220px;
  height: 220px;
  object-fit: cover;
  border-radius: 12px;
  flex: 0 0 auto;
  filter: grayscale(1) contrast(1.05);
  transition: filter .4s;
}

.gl-18__track img:hover {
  filter: none;
}

@keyframes gl-18-marquee {
  to {
    transform: translateX(-50%);
  }
}

@media (prefers-reduced-motion: reduce) {
  .gl-18__track {
    animation: none;
  }
}
/* No JavaScript — a doubled track translated -50% loops seamlessly and forever. */
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 Image Gallerie 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: CSS Infinite Scroll Image Strip
Source: https://codefronts.com/snippets/css-image-gallery/css-infinite-scroll-image-strip/

A seamless, endlessly looping horizontal banner of images that scrolls forever with no visible seam or reset — the marquee/logo-wall effect brand pages and 'as featured in' rows search for. Pure CSS keyframes.
## HTML
```html
<section class="gl-18" aria-label="Infinite scrolling image strip">
  <div class="gl-18__viewport">
    <div class="gl-18__track">
      <img src="https://loremflickr.com/300/300/architecture,mono?lock=1801" alt="Gallery image 1" loading="lazy" decoding="async">
      <img src="https://loremflickr.com/300/300/city,mono?lock=1802" alt="Gallery image 2" loading="lazy" decoding="async">
      <img src="https://loremflickr.com/300/300/nature,mono?lock=1803" alt="Gallery image 3" loading="lazy" decoding="async">
      <img src="https://loremflickr.com/300/300/portrait,mono?lock=1804" alt="Gallery image 4" loading="lazy" decoding="async">
      <img src="https://loremflickr.com/300/300/street,mono?lock=1805" alt="Gallery image 5" loading="lazy" decoding="async">
      <img src="https://loremflickr.com/300/300/mountain,mono?lock=1806" alt="Gallery image 6" loading="lazy" decoding="async">
      <img src="https://loremflickr.com/300/300/architecture,mono?lock=1801" alt="" aria-hidden="true" loading="lazy" decoding="async">
      <img src="https://loremflickr.com/300/300/city,mono?lock=1802" alt="" aria-hidden="true" loading="lazy" decoding="async">
      <img src="https://loremflickr.com/300/300/nature,mono?lock=1803" alt="" aria-hidden="true" loading="lazy" decoding="async">
      <img src="https://loremflickr.com/300/300/portrait,mono?lock=1804" alt="" aria-hidden="true" loading="lazy" decoding="async">
      <img src="https://loremflickr.com/300/300/street,mono?lock=1805" alt="" aria-hidden="true" loading="lazy" decoding="async">
      <img src="https://loremflickr.com/300/300/mountain,mono?lock=1806" alt="" aria-hidden="true" loading="lazy" decoding="async">
    </div>
  </div>
</section>
```
## CSS
```css
.gl-18,
.gl-18 *,
.gl-18 *::before,
.gl-18 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.gl-18 ::selection {
  background: oklch(0.7 0.14 30);
  color: #111;
}

.gl-18 {
  font-family: 'Segoe UI',system-ui,sans-serif;
  width: 100%;
  min-height: 100vh;
  padding: 40px 0;
  background: #0f0f0f;
  display: flex;
  align-items: center;
  justify-content: center;
}

.gl-18__viewport {
  width: 100%;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(90deg,transparent,#000 12%,#000 88%,transparent);
  mask-image: linear-gradient(90deg,transparent,#000 12%,#000 88%,transparent);
}

.gl-18__track {
  display: flex;
  gap: 16px;
  width: max-content;
  animation: gl-18-marquee 30s linear infinite;
}

.gl-18__viewport:hover .gl-18__track {
  animation-play-state: paused;
}

.gl-18__track img {
  display: block;
  width: 220px;
  height: 220px;
  object-fit: cover;
  border-radius: 12px;
  flex: 0 0 auto;
  filter: grayscale(1) contrast(1.05);
  transition: filter .4s;
}

.gl-18__track img:hover {
  filter: none;
}

@keyframes gl-18-marquee {
  to {
    transform: translateX(-50%);
  }
}

@media (prefers-reduced-motion: reduce) {
  .gl-18__track {
    animation: none;
  }
}
```

## JavaScript
```js
/* No JavaScript — a doubled track translated -50% loops seamlessly and forever. */
```

How this works

The track holds the image set twice back-to-back, then animates transform: translateX() from 0 to -50% on an infinite linear loop. Because the second half is an exact copy of the first, the moment the animation completes a cycle the strip is visually identical to its start — so the loop is seamless with no jump.

The strip pauses on hover so users can inspect an image, edge-fade masks soften the entry/exit, and prefers-reduced-motion stops the marquee entirely (a common accessibility requirement for auto-scrolling content).

Make it yours

  • Set speed with the animation duration; longer = slower drift.
  • Reverse direction by animating to +50% or using animation-direction: reverse.
  • Duplicate a second row scrolling the opposite way for a woven look.
  • Widen the edge-fade mask for a softer or harder vignette.

Gotchas — read before shipping

  • The content must be duplicated exactly and the translate must be -50% of the doubled track, or the loop visibly jumps.
  • Always provide a reduced-motion stop — perpetual auto-motion is an accessibility and INP concern.
  • Mark the duplicated copy aria-hidden="true" so screen readers don't announce every image twice.

Browser support

ChromeSafariFirefoxEdge
111+15.4+113+111+

Floor set by oklch() as this demo is written. The core technique itself goes back further — Chrome 43+.

Keyframe translate + mask edge-fade — universally supported (mask may need -webkit- in older Safari).

Techniques used in this demo

Search CodeFronts

Loading…