Dot Pagination Carousel — Free HTML CSS JS Slide Carousel Snippet
Dot Pagination Carousel · Carousels · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="carousel" id="carousel">
<div class="carousel-track" id="carouselTrack">
<div class="slide slide-1"><span>Slide 1</span></div>
<div class="slide slide-2"><span>Slide 2</span></div>
<div class="slide slide-3"><span>Slide 3</span></div>
<div class="slide slide-4"><span>Slide 4</span></div>
</div>
<button class="arrow arrow-prev" onclick="moveSlide(-1)" aria-label="Previous slide">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.5"><path d="M15 18l-6-6 6-6"/></svg>
</button>
<button class="arrow arrow-next" onclick="moveSlide(1)" aria-label="Next slide">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.5"><path d="M9 18l6-6-6-6"/></svg>
</button>
<div class="dots" id="dots" role="tablist" aria-label="Slide navigation">
<button class="dot active" onclick="goToSlide(0)" aria-label="Go to slide 1"></button>
<button class="dot" onclick="goToSlide(1)" aria-label="Go to slide 2"></button>
<button class="dot" onclick="goToSlide(2)" aria-label="Go to slide 3"></button>
<button class="dot" onclick="goToSlide(3)" aria-label="Go to slide 4"></button>
</div>
</div>* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; background: #f8fafc; padding: 24px; display: flex; align-items: center; flex-direction: column; gap: 20px; justify-content: center; min-height: 100vh; }
.carousel { position: relative; max-width: 480px; margin: 0 auto; border-radius: 14px; overflow: hidden; }
.carousel-track { display: flex; transition: transform 0.4s ease; }
.slide { min-width: 100%; height: 220px; display: flex; align-items: center; justify-content: center; }
.slide span { font-size: 20px; font-weight: 700; color: #fff; }
.slide-1 { background: linear-gradient(135deg, #6366f1, #4f46e5); }
.slide-2 { background: linear-gradient(135deg, #ec4899, #db2777); }
.slide-3 { background: linear-gradient(135deg, #10b981, #059669); }
.slide-4 { background: linear-gradient(135deg, #f59e0b, #d97706); }
.arrow {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 34px;
height: 34px;
border-radius: 50%;
border: none;
background: rgba(15,23,42,0.35);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background 0.15s;
}
.arrow:hover { background: rgba(15,23,42,0.55); }
.arrow-prev { left: 12px; }
.arrow-next { right: 12px; }
.dots { position: absolute; bottom: 14px; left: 50%; transform: translateX(-50%); display: flex; gap: 8px; }
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
border: none;
background: rgba(255,255,255,0.5);
cursor: pointer;
padding: 0;
transition: background 0.2s, width 0.2s;
}
.dot.active { background: #fff; width: 22px; border-radius: 4px; }const track = document.getElementById('carouselTrack');
const dots = document.querySelectorAll('.dot');
const totalSlides = document.querySelectorAll('.slide').length;
let currentIndex = 0;
function renderSlide() {
track.style.transform = 'translateX(-' + (currentIndex * 100) + '%)';
dots.forEach((dot, i) => dot.classList.toggle('active', i === currentIndex));
}
function goToSlide(index) {
currentIndex = index;
renderSlide();
}
function moveSlide(delta) {
currentIndex = (currentIndex + delta + totalSlides) % totalSlides;
renderSlide();
}
renderSlide();Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.
What's included
Features
About this UI Snippet
Dot Pagination Carousel — HTML, CSS & JavaScript Slide Carousel

Carousels are everywhere — hero banners, testimonial rotators, onboarding screens — and the dot-indicator-plus-arrows combination is the most recognizable navigation pattern for them. This snippet builds a complete carousel using a single sliding track and a row of clickable dots, with the currently active dot rendered as a wider pill rather than just a differently colored circle.
How the sliding mechanism works
All slides sit side by side inside .carousel-track with display: flex, each slide set to min-width: 100% so exactly one slide fills the visible .carousel viewport at a time (overflow: hidden on the parent hides the rest). Moving between slides is a single CSS transform: track.style.transform = 'translateX(-' + (currentIndex * 100) + '%)'. To show slide index 2, the track shifts left by exactly 200% of its own width — since each slide is 100% of the viewport, sliding by a multiple of 100% always lines up a slide exactly with the viewport. The transition: transform 0.4s ease on the track is what makes that jump animate smoothly instead of snapping.
How the dots and arrows share one state
There is exactly one source of truth: the currentIndex variable. Both goToSlide(index) (called by clicking a dot) and moveSlide(delta) (called by clicking an arrow, passing +1 or -1) do nothing but update currentIndex and then call the single renderSlide() function, which both moves the track and updates which dot has the .active class. This means dots and arrows can never disagree about which slide is showing — they're just two different ways of changing the same variable.
How the arrows wrap around
moveSlide computes (currentIndex + delta + totalSlides) % totalSlides. Adding totalSlides before the modulo is what correctly handles the "previous" click from slide 0 — without it, (0 - 1) % 4 would evaluate to -1 in JavaScript (a negative number, not a valid array-style wraparound), rather than the desired 3.
How the active dot pill effect works
The .dot.active class doesn't just change background — it also grows width from 8px to 22px and rounds the corners slightly less (border-radius: 4px instead of 50%), turning a circle into a short pill. This is a purely cosmetic CSS transition layered on top of the same class toggle used for the color change.
Step by step
How to Use
- 1Load the snippetClick "Dot Pagination Carousel" in the sidebar Library tab to load the four-slide demo.
- 2Click dots and arrowsClick any dot to jump directly to that slide, or use the prev/next arrows in the preview.
- 3Add real contentReplace each .slide div's background gradient with a real image via background-image or an <img> tag.
- 4Add more slidesAdd a new .slide div in the track and a matching dot button — totalSlides and the wraparound math update automatically.
- 5Add autoplayWrap moveSlide(1) in a setInterval to auto-advance, and clear it on user interaction if you want to pause on hover.
- 6Export and saveExport as HTML/JSX/Tailwind, or click "Save as" to reuse this carousel in a real hero or testimonial section.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Each slide is exactly 100% of the visible viewport width. Sliding to index N means shifting the track left by N times 100%, which the transform property does directly: translateX(-N00%). Because every slide is the same width, this math works for any number of slides without per-slide coordinates.
Both interactions only ever change one shared variable, currentIndex, and then call the same renderSlide function, which updates both the track position and the active dot. There is no separate state for "which dot is active" versus "which slide is showing" — they are driven by the same source of truth.
moveSlide computes (currentIndex + delta + totalSlides) % totalSlides. Adding totalSlides before the modulo operation ensures a negative intermediate value (like -1 when going back from slide 0) wraps around to the correct last-slide index instead of producing a negative number.
Wrap a call to moveSlide(1) in a setInterval with your desired delay (e.g. 4000ms). Consider clearing and restarting that interval on manual dot/arrow interaction, and pausing it on mouse hover or keyboard focus for better usability.
Yes — replace each .slide div's background gradient with a background-image, or replace the div's content with an <img> element sized to fill the slide with object-fit: cover.
Add a new .slide div inside the track and a matching dot button in the .dots container calling goToSlide with the next index. totalSlides is calculated automatically from the DOM, so no manual count needs updating.
The dots and arrows are native buttons with descriptive aria-labels ("Go to slide 2", "Previous slide"), and the dots container uses role="tablist" to signal a navigable group. All controls are reachable and operable via keyboard by default since they are real button elements.