Blog
Pure CSS Carousel with Arrows Navigation
Build a responsive image carousel with previous and next links using only HTML and CSS. This example uses native horizontal scrolling and CSS scroll snap, so it works with touch gestures and does not need JavaScript or a fixed slide width. Quick answer: put each slide in a horizontally scrollable flex track, setscroll-snap-type: x mandatory on the track, and give every slide scroll-snap-align: start. Link the arrow controls to each slide’s ID so visitors can move through the carousel with normal browser navigation.
Try the Live DemoDownload the Source ZIP
What this CSS-only carousel does
The carousel keeps the browser’s native scrolling behavior and adds snap points so each slide settles neatly into view. The arrow links jump to slide anchors, while the track remains swipeable on phones and scrollable with a keyboard. Because there is no script, the example avoids JavaScript initialization and continues to work when scripts are unavailable.How the carousel works
- Use a scroll container: the track uses
display: flexandoverflow-x: autoto place slides in a row. - Snap to each slide:
scroll-snap-type: x mandatoryandscroll-snap-align: startkeep the slide aligned after a swipe or scroll. - Navigate with links: each arrow points to a slide ID. Give the links clear accessible names so assistive technology can identify the destination.
- Keep it responsive: slides use a flexible basis and a clamped minimum height instead of a fixed pixel width.
HTML for the carousel
Create one track and give every slide a unique ID. Change the sample slide content to your own images; use descriptive alt text for meaningful images.CSS for responsive scrolling and arrows
The CSS below creates the scroll-snap track, slide cards, arrow controls, focus styles, and reduced-motion behavior. The demo uses CSS gradients for its artwork so it runs without external image requests.Accessibility and browser behavior
Native horizontal scrolling provides touch and keyboard support. Keep the track focusable, label the carousel and slides, and make arrow links visibly focusable. The browser may scroll the page to an anchor when an arrow is activated; this is expected native behavior. If you need autoplay, synchronized indicators, or complex looping, use JavaScript and provide pause controls rather than disguising automatic motion as a CSS-only feature.Customize the carousel
- Replace the demo gradients with responsive
<img>elements and useful alt text. - Adjust card height and border radius to match your layout.
- Add more slides by duplicating a slide, assigning a unique ID, and updating neighboring arrow targets.
- For a multi-card layout, reduce each slide’s flex-basis and adjust scroll-snap alignment.
Common issues
Arrows jump to the wrong slide: check that each link’s fragment matches an existing, unique ID. Slides do not snap: placescroll-snap-type on the actual scrolling track and scroll-snap-align on its direct slide children.
Carousel overflows the page: set min-width: 0 on flex or grid parents and keep horizontal overflow on the track.
Images look stretched: use object-fit: cover and a consistent aspect ratio for each image.
FAQ
Can I make an image carousel with CSS only?
Yes. A horizontally scrollable track with CSS scroll snap creates a CSS-only carousel. Anchor links can provide previous and next navigation without JavaScript.Does this carousel autoplay?
No. It is intentionally user controlled. This avoids unexpected motion and keeps the simple demo focused on navigation.Can users swipe the carousel on mobile?
Yes. The track uses native horizontal scrolling, so touch swipes work without a JavaScript gesture handler.How do I add more slides?
Add another slide element with a unique ID, then update the previous and next anchor links to point to the correct slide IDs.Interactive example
CSS-only image carousel
Use the arrows, swipe horizontally, or choose a dot. This demo uses native scrolling and CSS scroll snap—no JavaScript.
Tip: Focus the carousel and scroll horizontally with a keyboard or trackpad. Reduced-motion preferences are respected.
Carousel HTML markup
<section class="carousel" aria-label="Scenic image carousel">
<div class="track" id="carousel-track" tabindex="0" aria-label="Carousel slides">
<article class="slide slide-1" id="slide-1" aria-label="Slide 1 of 3">
<h2>Coastal escape</h2>
<a class="arrow prev" href="#slide-3" aria-label="Previous slide: Golden hour">‹</a>
<a class="arrow next" href="#slide-2" aria-label="Next slide: Evening skies">›</a>
</article>
<article class="slide slide-2" id="slide-2" aria-label="Slide 2 of 3">
<h2>Evening skies</h2>
<a class="arrow prev" href="#slide-1" aria-label="Previous slide: Coastal escape">‹</a>
<a class="arrow next" href="#slide-3" aria-label="Next slide: Golden hour">›</a>
</article>
<article class="slide slide-3" id="slide-3" aria-label="Slide 3 of 3">
<h2>Golden hour</h2>
<a class="arrow prev" href="#slide-2" aria-label="Previous slide: Evening skies">‹</a>
<a class="arrow next" href="#slide-1" aria-label="Next slide: Coastal escape">›</a>
</article>
</div>
<nav class="dots" aria-label="Choose a slide">
<a href="#slide-1" aria-label="Show slide 1"></a>
<a href="#slide-2" aria-label="Show slide 2"></a>
<a href="#slide-3" aria-label="Show slide 3"></a>
</nav>
</section>
<div class="track" id="carousel-track" tabindex="0" aria-label="Carousel slides">
<article class="slide slide-1" id="slide-1" aria-label="Slide 1 of 3"><h2>Coastal escape</h2></article>
<article class="slide slide-2" id="slide-2" aria-label="Slide 2 of 3"><h2>Evening skies</h2></article>
<article class="slide slide-3" id="slide-3" aria-label="Slide 3 of 3"><h2>Golden hour</h2></article>
</div>
<a class="arrow prev" href="#slide-3" aria-label="Previous slide: Golden hour">‹</a>
<a class="arrow next" href="#slide-2" aria-label="Next slide: Evening skies">›</a>
</section>
<nav class="dots" aria-label="Choose a slide">
<a href="#slide-1" aria-label="Show slide 1"></a>
<a href="#slide-2" aria-label="Show slide 2"></a>
<a href="#slide-3" aria-label="Show slide 3"></a>
</nav>
Responsive carousel CSS
.carousel {
max-width: 900px;
margin: 30px auto;
overflow: hidden;
border-radius: 16px;
background: #0e1830;
}
.track {
display: flex;
overflow-x: auto;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
scrollbar-width: none;
}
.track::-webkit-scrollbar {
display: none;
}
.slide {
position: relative;
isolation: isolate;
display: grid;
flex: 0 0 100%;
min-height: clamp(280px, 52vw, 480px);
place-items: center;
overflow: hidden;
scroll-snap-align: start;
color: #fff;
}
.slide::before {
position: absolute;
z-index: -1;
inset: 0;
content: "";
background: var(--scene);
}
.slide::after {
position: absolute;
z-index: -1;
inset: 35% 0 0;
content: "";
background: linear-gradient(transparent, #081226c9);
}
.slide h2 {
align-self: end;
margin: 0 24px 46px;
color: #fff;
font-size: clamp(1.5rem, 5vw, 3rem);
text-shadow: 0 2px 18px #0008;
}
.slide-1 {
--scene: radial-gradient(ellipse at 72% 32%, #ffe7a2 0 5%, transparent 6%), linear-gradient(160deg, transparent 50%, #125252 51% 65%, #0a3146 66%), linear-gradient(180deg, #86d3e8, #5679bf 64%, #f5b77c);
}
.slide-2 {
--scene: linear-gradient(155deg, transparent 45%, #183d4d 46% 57%, #0d2939 58%), linear-gradient(180deg, #ffcda0, #9b8bcc 55%, #3f638a);
}
.slide-3 {
--scene: radial-gradient(ellipse at 70% 35%, #ffdd8c 0 7%, transparent 8%), linear-gradient(165deg, transparent 48%, #536c48 49% 63%, #243b37 64%), linear-gradient(180deg, #f2a8a0, #727ec1 60%, #202e54);
}
.arrow {
position: absolute;
top: 50%;
z-index: 2;
display: grid;
width: 46px;
height: 46px;
place-items: center;
border: 1px solid #ffffffb8;
border-radius: 50%;
background: #101b35b8;
color: #fff;
font-size: 1.5rem;
line-height: 1;
text-decoration: none;
transform: translateY(-50%);
transition: background 0.2s, transform 0.2s;
}
.arrow:hover,
.arrow:focus-visible {
background: #315ec9;
transform: translateY(-50%) scale(1.07);
}
.arrow:focus-visible,
.dots a:focus-visible {
outline: 3px solid #315ec9;
outline-offset: 3px;
}
.prev {
left: 14px;
}
.next {
right: 14px;
}
.dots {
display: flex;
justify-content: center;
gap: 10px;
padding: 18px;
}
.dots a {
width: 10px;
height: 10px;
border-radius: 50%;
background: #c7d2e4;
}
@media (prefers-reduced-motion: reduce) {
.track {
scroll-behavior: auto;
}
.arrow {
transition: none;
}
}
position: relative;
overflow: hidden;
border-radius: 16px;
background: #0e1830;
}
.track {
display: flex;
overflow-x: auto;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
scrollbar-width: none;
}
.track::-webkit-scrollbar { display: none; }
.slide {
position: relative;
flex: 0 0 100%;
min-height: clamp(280px, 52vw, 480px);
scroll-snap-align: start;
display: grid;
place-items: center;
isolation: isolate;
overflow: hidden;
color: #fff;
}
.slide::before {
position: absolute;
z-index: -1;
inset: 0;
content: "";
background: var(--scene);
}
.slide-1 { --scene: linear-gradient(180deg, #86d3e8, #5679bf 64%, #f5b77c); }
.slide-2 { --scene: linear-gradient(180deg, #ffcda0, #9b8bcc 55%, #3f638a); }
.slide-3 { --scene: linear-gradient(180deg, #f2a8a0, #727ec1 60%, #202e54); }
.arrow {
position: absolute;
top: 50%;
z-index: 2;
display: grid;
width: 46px;
height: 46px;
place-items: center;
border: 1px solid #ffffffb8;
border-radius: 50%;
background: #101b35b8;
color: #fff;
text-decoration: none;
transform: translateY(-50%);
}
.arrow:focus-visible { outline: 3px solid #fff; outline-offset: 3px; }
.prev { left: 14px; }
.next { right: 14px; }
@media (prefers-reduced-motion: reduce) {
.track { scroll-behavior: auto; }
}