What Scroll Snap Actually Solves
Before scroll-snap-type, carousels and paginated sections required either a JavaScript carousel library or manual scrollLeft/scrollTop math tied to touch and wheel event listeners. CSS scroll snap moved that entirely into the browser's native scrolling and gesture-handling pipeline, which means it inherits accelerated scrolling, momentum, and accessibility behavior for free.
The core mental model: a scroll container declares that it wants to snap, and its children declare where the snap points are.
.container {
scroll-snap-type: x mandatory;
overflow-x: auto;
display: flex;
}
.item {
scroll-snap-align: start;
flex: 0 0 100%;
}
That's a complete, functioning full-width carousel. No JS.
The Properties, in Practice
scroll-snap-type: axis and strictness
scroll-snap-type: x mandatory; /* horizontal, always snaps */
scroll-snap-type: y proximity; /* vertical, snaps loosely */
scroll-snap-type: both mandatory; /* 2D grid-style snapping */
mandatory guarantees the container comes to rest on a snap point. proximity only snaps if the natural scroll deceleration ends near one — a fast flick can land between items. For image carousels and full-page sections, mandatory is almost always what users expect. For long vertically-scrolling lists (like a feed with occasional snap-worthy cards), proximity feels less jarring.
scroll-snap-align: where each child snaps
.item { scroll-snap-align: start; } /* item's start edge aligns to container's snap area */
.item { scroll-snap-align: center; } /* item centers in the container */
.item { scroll-snap-align: end; } /* item's end edge aligns */
center is the right choice for "focused item" carousels — image lightboxes, testimonial sliders — where you want the active item visually centered rather than flush against an edge.
scroll-padding and scroll-margin: accounting for sticky headers
A common bug: scroll snap lands items exactly at the container's edge, which is wrong when you have a sticky header overlapping the top of the scroll area.
.container {
scroll-snap-type: y mandatory;
scroll-padding-top: 4rem; /* height of the sticky header */
}
scroll-padding shifts where "start" means, without changing layout — the equivalent of scroll-margin-top but applied to the container rather than each item. Use scroll-margin on individual items instead when different children need different offsets (e.g., the first card needs extra clearance but the rest don't).
Full Working Example: Horizontal Card Carousel
<div class="carousel">
<div class="carousel-item">1</div>
<div class="carousel-item">2</div>
<div class="carousel-item">3</div>
<div class="carousel-item">4</div>
</div>
.carousel {
display: flex;
gap: 1rem;
overflow-x: auto;
scroll-snap-type: x mandatory;
scroll-padding-inline: 1rem;
padding-inline: 1rem;
/* hide scrollbar while keeping it functional */
scrollbar-width: none;
}
.carousel::-webkit-scrollbar {
display: none;
}
.carousel-item {
scroll-snap-align: start;
scroll-snap-stop: always;
flex: 0 0 min(80%, 320px);
aspect-ratio: 4 / 3;
border-radius: 0.75rem;
box-shadow: 0 4px 12px rgb(0 0 0 / 0.12);
}
scroll-snap-stop: always forces the browser to stop at every snap point in sequence even during a fast fling, rather than skipping past several items — worth setting explicitly on carousels where skipping items would disorient the user. If you're tuning that card shadow value, the Box Shadow Generator is a quick way to preview elevation without round-tripping through DevTools each time.
Full-Page Vertical Sections
html {
scroll-snap-type: y mandatory;
height: 100%;
overflow-y: scroll;
}
section {
height: 100vh;
scroll-snap-align: start;
}
This is the "one section per screen" pattern used on landing pages. Watch for one subtlety: putting scroll-snap-type on html rather than a wrapper div avoids double-scrollbar issues that show up when a nested container also has overflow, but it also means the whole page loses normal free scrolling — test carefully on content-heavy sections that might need to scroll internally.
Decision Point: Scroll Snap vs a JS Carousel Library
Use native CSS scroll snap when:
- The interaction is fundamentally "swipe/scroll to move between items" — carousels, image galleries, full-page sections, horizontally-scrolling card lists.
- You want native touch/trackpad/keyboard scrolling behavior rather than reimplementing it.
- You don't need infinite looping, autoplay, or complex custom transition easing between slides.
- Bundle size matters — scroll snap adds zero JavaScript.
Reach for a JS carousel library when:
- You need infinite/looping carousels (scroll snap has no native loop-back).
- You need autoplay with pause-on-hover/interaction.
- You need fade or custom transform-based transitions instead of physical scrolling.
- You need synchronized multi-carousel behavior (e.g., a thumbnail strip driving a main image viewer).
A hybrid is common and often the right call: use scroll snap for the physical scrolling mechanism and layer a small amount of JS on top only for pagination dots or programmatic "next" buttons, using scrollTo({ behavior: 'smooth' }) against the snap points rather than reimplementing scrolling from scratch.
document.querySelector('.next-btn').addEventListener('click', () => {
const container = document.querySelector('.carousel');
const item = container.querySelector('.carousel-item');
container.scrollBy({ left: item.offsetWidth + 16, behavior: 'smooth' });
});
Mobile and Cross-Browser Notes
Scroll snap has solid support across modern browsers, including iOS Safari and Chrome for Android, but a few things trip people up on mobile specifically:
- Nested scroll-snap containers (a snapping carousel inside a snapping vertical page) can produce inconsistent gesture handling — test on real devices, not just DevTools device emulation.
scroll-snap-stop: alwayson an item taller than the viewport can trap the user's scroll gesture inside that one section. Only use it on items that fit within a single screen or scroll gesture.- Always pair horizontal scroll-snap containers with visible affordances (partial next-item peeking, or explicit arrows) — a full-bleed snapping carousel with no visual cue that more content exists is easy for users to miss entirely.
Start with a single-axis carousel, confirm snapping feels right with real touch and trackpad input, then layer in padding/margin adjustments for sticky headers before shipping.