Illustration of a seamless CSS image slider with repeating landscape cards
Blog

Create a Seamless Infinite CSS Slider Animation

Build a continuously scrolling image slider with HTML and CSS only. This version duplicates one slide group to create a seamless loop, pauses when visitors hover or move keyboard focus to it, and turns off automatic motion for people who prefer reduced motion. No JavaScript or carousel library is required.

What makes an infinite slider seamless?

A loop looks continuous when the end of the moving track lines up exactly with its beginning. This demo places two identical groups side by side and moves the track by half of its full width. When the second group reaches the first group’s starting position, the animation repeats without a visible jump.

Try the CSS image slider

Watch the cards move across the demo. Hover over the carousel or focus it with the keyboard to pause. When reduced motion is enabled in your device settings, the animation stops and you can scroll the cards manually.

How the CSS animation works

The track is a flex row with a fixed-width group of cards. The second group repeats the first, so the whole track is exactly twice as wide as one group. The @keyframes rule translates the track by -50%; linear keeps its speed steady and infinite repeats the movement.

The carousel pauses with animation-play-state: paused on hover and keyboard focus. A prefers-reduced-motion media query removes the animation and duplicate group, leaving a manually scrollable row. This keeps the visual effect usable without requiring JavaScript.

Use your own images

The sample uses CSS gradients so it works immediately without external files. To use photographs, replace each decorative .scene element with an image, give the image meaningful alternative text when it conveys content, and set object-fit: cover so each card keeps its shape. Use appropriately sized, compressed images to keep the slider fast.

Customize the slider

  • Adjust the 30s duration on .slider-track to change the speed.
  • Change the card width, height, gap, and corner radius to match your layout.
  • Keep both slide groups identical, including spacing, so the loop does not jump.
  • Keep the pause-on-hover and pause-on-focus rules for better control.
  • Keep the reduced-motion fallback so visitors can scroll without automatic movement.

Common issues

The loop jumps at the end

Check that the second group is an exact duplicate of the first and that both groups use the same card widths and gaps. The track should move by one group’s width, which is half the complete track.

The cards move at different speeds or leave a gap

Use a single animated track and avoid independent animations on individual cards. Make the slide groups flex items that do not shrink, and include the same trailing spacing in both groups.

The slider is too fast on a phone

Increase the animation duration and use a responsive card width. The downloadable example also disables the animation when reduced motion is requested, while preserving manual scrolling.

Frequently asked questions

Can I make an infinite slider using only CSS?

Yes. CSS keyframes can move a duplicated row of slides continuously. HTML supplies the cards, and CSS controls their layout, animation, pause states, and reduced-motion fallback.

Why does this example repeat the slides?

The second identical group fills the space left behind by the first. Moving the full track by exactly half its width lets the second group take the first group’s place before the animation restarts.

Does the slider need JavaScript?

No. The carousel in this tutorial is CSS-only. JavaScript is useful when you need buttons, pagination, dynamic content, or programmatic slide controls.

Summary

This infinite CSS slider uses two matching card groups and one linear keyframe animation to create a seamless loop. It pauses on hover and keyboard focus and switches to manual scrolling for reduced-motion users. Download the HTML and CSS files to customize the demo.

CSS-only live demo

Infinite CSS image slider

Two matching slide groups create a continuous loop. Hover or focus this carousel to pause it.

Dawn CoastSoft light over the water
Alpine LakeStill water, open skies
Desert BloomWarm color at sunset
Night CityLights across the skyline
Forest TrailA path through the pines
Ocean BreezeBlue waves meet the shore

The loop pauses on hover and keyboard focus. With reduced motion enabled, animation stops and the cards can be scrolled manually.

HTML: Duplicate slide groups for a seamless loop
<div class="slider-window" role="region" aria-label="Featured places" aria-roledescription="carousel" tabindex="0">
  <div class="slider-track">
    <div class="slide-group">
      <article class="slide slide-coast"><span class="scene" aria-hidden="true"></span><h3>Dawn Coast</h3></article>
      <article class="slide slide-lake"><span class="scene" aria-hidden="true"></span><h3>Alpine Lake</h3></article>
      <article class="slide slide-desert"><span class="scene" aria-hidden="true"></span><h3>Desert Bloom</h3></article>
    </div>
    <div class="slide-group" aria-hidden="true">
      <article class="slide slide-coast"><span class="scene"></span><h3>Dawn Coast</h3></article>
      <article class="slide slide-lake"><span class="scene"></span><h3>Alpine Lake</h3></article>
      <article class="slide slide-desert"><span class="scene"></span><h3>Desert Bloom</h3></article>
    </div>
  </div>
</div>
CSS: Seamless loop, pause controls, reduced motion
:root { --slide-width: clamp(210px, 27vw, 270px); --slider-gap: 16px; }

.slider-window { overflow: hidden; padding: 8px 0 18px; border-radius: 18px; outline: none; }
.slider-window:focus-visible { outline: 3px solid #8398ff; outline-offset: 4px; }
.slider-track { display: flex; width: max-content; animation: slider-loop 30s linear infinite; will-change: transform; }
.slide-group { display: flex; flex: 0 0 auto; gap: var(--slider-gap); padding-right: var(--slider-gap); }
.slide { position: relative; isolation: isolate; display: flex; flex: 0 0 var(--slide-width); height: 210px; flex-direction: column; justify-content: flex-end; overflow: hidden; padding: 19px; border-radius: 17px; color: #fff; box-shadow: 0 16px 35px rgb(37 50 87 / 15%); }
.slide::before { position: absolute; z-index: -2; inset: 0; content: ""; background: linear-gradient(180deg, transparent 25%, rgb(10 20 43 / 75%)); }
.slide h3 { margin: 0; font-size: 1.05rem; }
.scene { position: absolute; z-index: -3; inset: 0; }
.slide-coast .scene { background: linear-gradient(155deg, #f7b87c, #ec8291 42%, #6b82c3 43%, #345f98); }
.slide-lake .scene { background: linear-gradient(160deg, #9ad9de, #6daec9 48%, #345786 49%, #243b68); }
.slide-desert .scene { background: linear-gradient(150deg, #ffd17e, #f49b75 46%, #9e5577 47%, #513f78); }
.slide-city .scene { background: linear-gradient(150deg, #172a57, #485596 54%, #e77891 55%, #f5b775); }
.slide-forest .scene { background: linear-gradient(155deg, #9ec3a2, #5e9e8a 45%, #356759 46%, #224c4a); }
.slide-ocean .scene { background: linear-gradient(155deg, #8bd5dc, #55aaca 42%, #3676a8 43%, #19487b); }
@keyframes slider-loop { to { transform: translateX(-50%); } }
@media (hover: hover) { .slider-window:hover .slider-track { animation-play-state: paused; } }
.slider-window:focus-within .slider-track { animation-play-state: paused; }
@media (prefers-reduced-motion: reduce) {
  .slider-window { overflow-x: auto; scroll-snap-type: x mandatory; }
  .slider-track { width: max-content; animation: none; transform: none; }
  .slide-group[aria-hidden="true"] { display: none; }
  .slide { scroll-snap-align: start; }
}