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
30sduration on.slider-trackto 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.
The loop pauses on hover and keyboard focus. With reduced motion enabled, animation stops and the cards can be scrolled manually.
<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>
: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; }
}