Native Scroll-Snap Carousel — HTML CSS JS Snippet
Native Scroll-Snap Carousel · Carousels · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="nss-wrap">
<div class="nss-track" id="nssTrack">
<div class="nss-slide" style="background:linear-gradient(160deg,#6366f1,#4338ca)">1</div>
<div class="nss-slide" style="background:linear-gradient(160deg,#0ea5e9,#0369a1)">2</div>
<div class="nss-slide" style="background:linear-gradient(160deg,#ec4899,#9d174d)">3</div>
<div class="nss-slide" style="background:linear-gradient(160deg,#10b981,#047857)">4</div>
<div class="nss-slide" style="background:linear-gradient(160deg,#f59e0b,#b45309)">5</div>
</div>
<div class="nss-dots" id="nssDots"></div>
</div>*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#f6f7f9;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.nss-wrap{width:100%;max-width:420px}
.nss-track{display:flex;overflow-x:auto;scroll-snap-type:x mandatory;border-radius:16px;scrollbar-width:none}
.nss-track::-webkit-scrollbar{display:none}
.nss-slide{flex:0 0 100%;scroll-snap-align:center;height:240px;display:flex;align-items:center;justify-content:center;font-size:56px;font-weight:800;color:#fff}
.nss-dots{display:flex;justify-content:center;gap:7px;margin-top:14px}
.nss-dot{width:7px;height:7px;border-radius:50%;background:#d1d5db;border:none;cursor:pointer;transition:background .2s,width .2s}
.nss-dot.active{background:#6366f1;width:20px;border-radius:4px}var track = document.getElementById('nssTrack');
var slides = document.querySelectorAll('.nss-slide');
var dotsWrap = document.getElementById('nssDots');
slides.forEach(function (s, i) {
var d = document.createElement('button');
d.className = 'nss-dot';
d.setAttribute('aria-label', 'Go to slide ' + (i + 1));
d.addEventListener('click', function () {
// scrollIntoView respects scroll-snap-align, so this both scrolls AND
// lands exactly on the native snap point — no manual offset math needed.
slides[i].scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
});
dotsWrap.appendChild(d);
});
var dots = document.querySelectorAll('.nss-dot');
// IntersectionObserver watches which slide the native scroll has actually
// settled on, so the dots reflect real scroll position — including from a
// user's own trackpad/touch scroll, not just our own button clicks.
var observer = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting && entry.intersectionRatio > 0.6) {
var index = Array.prototype.indexOf.call(slides, entry.target);
dots.forEach(function (d, i) { d.classList.toggle('active', i === index); });
}
});
}, { root: track, threshold: [0.6] });
slides.forEach(function (s) { observer.observe(s); });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
Native Scroll-Snap Carousel — The Browser Does the Sliding

Every other slide-based carousel in this library computes its own translateX and handles its own drag/momentum. This one does neither — scroll-snap-type: x mandatory on the track and scroll-snap-align: center on each slide tell the *browser's own scrolling engine* to snap to slide boundaries, which means momentum, touch, trackpad, and keyboard scrolling are all free, genuinely native behaviors, not reimplemented ones.
JavaScript only ever watches, never moves
With the sliding itself fully delegated to CSS, the JS in this snippet has exactly two jobs: let a dot click *request* a scroll (via scrollIntoView, which respects the same snap alignment automatically) and *observe* which slide the browser has actually settled on afterward, via IntersectionObserver. Neither job ever writes a transform or tracks a pointer position — there's no "carousel state" to keep in sync with the DOM at all, because the DOM's own scroll position *is* the state.
Why IntersectionObserver instead of a scroll listener
Detecting "which slide is currently active" from a raw scroll event means computing scroll offsets on every single scroll frame — exactly the kind of layout-thrashing work IntersectionObserver exists to avoid. Here, each slide is watched with a threshold: [0.6], so the observer's callback only fires when a slide crosses 60% visibility — a natural, cheap way to say "this one is now the settled slide," whether the scroll was triggered by a button, a swipe, or a trackpad.
Step by step
How to Use
- 1Paste HTML, CSS, and JSFive full-width slides appear, with the first dot highlighted.
- 2Swipe or scroll the trackNative momentum scrolling moves between slides, snapping cleanly to each one — no JS drag logic involved.
- 3Click a dotThe browser smoothly scrolls to that slide via scrollIntoView, which honors the same snap alignment.
- 4Scroll with a trackpad or mouse wheelWorks identically, since it's genuinely native scrolling, not a simulated gesture.
- 5Add a sixth slideAdd one .nss-slide div — a matching dot is generated automatically and observed for snap detection.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
For a plain "one slide at a time, swipeable" carousel, native scroll-snap gives momentum, touch, and keyboard scrolling for free from the browser — a JS-driven carousel has to reimplement all three by hand for equivalent behavior, which is only worth it when you need effects scroll-snap can't express (3D, elastic, magnetic, etc).
Change .nss-slide's flex-basis from 100% to a fraction like 50% or 33.33% — scroll-snap-align still works per-slide, so partial-width slides snap just as cleanly.
Yes — give each arrow a click handler that calls track.scrollBy({ left: track.clientWidth, behavior: "smooth" }) (or negative for previous); the snap points still apply since it's still native scrolling.
scrollIntoView already knows to align to the slide's scroll-snap-align setting and animates via the browser's own smooth-scroll implementation — hand-calculating an offset would just be reproducing logic the browser already provides correctly.
Yes — because scrolling is native, the track is keyboard-scrollable by default (Tab into it, then arrow keys or Page Up/Down), and dots are real labeled buttons offering an additional, more discoverable navigation path.