Build carousels and galleries natively in the browser, with zero JavaScript library
Scroll Snap brings the behavior of classic carousel libraries straight into the browser: the user scrolls freely, but the container locks into defined positions cleanly and without jank. Combine scroll-snap-type and scroll-snap-align correctly and you drop an entire JavaScript dependency while getting touch gestures, keyboard control and accessibility essentially for free.
Table of Contents
- 1. What CSS Scroll Snap is and which problem it solves
- 2. scroll-snap-type: mandatory versus proximity and picking the scroll axis
- 3. scroll-snap-align: defining snap points on the child elements
- 4. scroll-padding and scroll-margin: correcting distance to the snap point
- 5. Recipe: a horizontal product carousel without a library
- 6. Vertical snap for storytelling pages and full-screen sections
- 7. Accessibility: thinking keyboard, touch and scroll snap together
- 8. Browser support and fallback strategies
- 9. When CSS Scroll Snap is enough and when a library still makes sense
- 10. Summary
- 11. FAQ
1. What CSS Scroll Snap is and which problem it solves
Scroll Snap is a CSS module that tells the browser where a scroll container should lock into place after the mouse, trackpad or finger is released. Instead of calculating a carousel with JavaScript, intercepting touch events and manually interpolating animations, the browser handles this work itself, directly in the native rendering path, and typically far smoother than any JS solution can manage.
Before Scroll Snap, every carousel on a website needed an external library with its own CSS, its own JavaScript logic and usually several extra kilobytes of code, just to simulate horizontal scrolling with snap points. With scroll-snap-type and scroll-snap-align, a handful of CSS lines achieve the same behavior natively, including keyboard navigation and momentum scrolling on touch devices that a JS solution would have to painstakingly rebuild.
2. scroll-snap-type: mandatory versus proximity and picking the scroll axis
The scroll-snap-type property belongs on the scrolling container and defines two things: the axis (x, y, or both) and how strict the snapping is. With mandatory, the container always locks to the nearest snap position after every scroll gesture, even if the user only scrolled a tiny amount. With proximity, it only snaps when already close enough to a snap position, otherwise the scroll position stays free.
For product carousels, mandatory is almost always the right choice, because users expect every swipe to reveal a complete item rather than leaving half-cut cards on screen. For long, freely scrollable image galleries with many items, proximity is often more pleasant, since it lets the user scroll through freely and only gently snaps near the end, instead of hard-stopping on every small scroll impulse.
/* Horizontal container: mandatory snap on the X axis */
.carousel {
display: flex;
overflow-x: auto;
scroll-snap-type: x mandatory;
gap: 1rem;
/* avoids jank on programmatic scrolling */
scroll-behavior: smooth;
}
/* Long gallery: proximity is less intrusive */
.gallery {
overflow-x: auto;
scroll-snap-type: x proximity;
}
3. scroll-snap-align: defining snap points on the child elements
While scroll-snap-type controls the container, scroll-snap-align is set on every individual child element and defines which edge it should snap to: start, center, or end. For a product carousel where every card should be visible flush left, start is the usual choice. For an image carousel that should center each image, center fits better, because it leaves an equal amount of the neighboring item visible on both sides.
It matters that scroll-snap-align is set on each child, not the container. Forget it, and the container still scrolls freely but never snaps, because the browser has no information about where the snap points actually are. This combination of container and child properties is the most common stumbling block when setting up scroll snap for the first time.
<div class="carousel">
<div class="card" style="scroll-snap-align: start;">Card 1</div>
<div class="card" style="scroll-snap-align: start;">Card 2</div>
<div class="card" style="scroll-snap-align: start;">Card 3</div>
</div>
<style>
.card { flex: 0 0 280px; scroll-snap-align: start; }
</style>
4. scroll-padding and scroll-margin: correcting distance to the snap point
If a sticky header or other fixed element sits above the carousel, the browser still snaps cards exactly to the container edge, even if that edge visually disappears behind the header. scroll-padding on the container fixes that by defining a virtual safety margin from the container edge, exactly like scroll-margin-top does for anchor jump targets under a sticky header.
scroll-margin works the opposite way on the child element: it shifts the individual snap point of a single card, independent of the container's global padding. This is useful when, for example, only the first or last card in a row needs extra spacing, such as leaving visible breathing room from the screen edge as a scroll affordance.
.carousel {
scroll-padding-inline: 1rem; /* distance from the container edge */
}
.card:first-child {
scroll-margin-inline-start: 1rem; /* individual snap offset */
}
5. Recipe: a horizontal product carousel without a library
A complete product carousel needs, besides snap itself, only overflow-x: auto, a fixed or minimum card width with flex: 0 0 <width>, and enough gap between cards. The scrollbar itself can be visually hidden with scrollbar-width: none plus the WebKit equivalent, while scroll functionality remains fully intact, because touch and keyboard scrolling work independently of the visible scrollbar.
The decisive advantage over a JS library shows up on the very first interaction frame: a native scroll-snap carousel responds instantly to the first touch, because the browser's scroll handler is already built in. A JS solution first has to be loaded, parsed and initialized before it can react to input at all, which produces noticeable delay especially on mobile devices.
.carousel {
display: flex;
overflow-x: auto;
gap: 1rem;
scroll-snap-type: x mandatory;
scrollbar-width: none;
}
.carousel::-webkit-scrollbar { display: none; }
.card {
flex: 0 0 clamp(220px, 70vw, 320px);
scroll-snap-align: start;
}
6. Vertical snap for storytelling pages and full-screen sections
On the Y axis, scroll-snap-type: y mandatory fits storytelling pages that jump from section to section, similar to presentation slides. Every section gets height: 100vh and scroll-snap-align: start, so the user always sees a complete section while scrolling, never a half-way transition between two sections.
A common mistake with vertical snap is applying it to long text pages with variable content length. If a section is taller than the viewport, mandatory can make reading harder, because the browser keeps trying to snap to the defined points even though the user is actually still reading further down inside a section. For those cases, proximity, or skipping snap entirely, is the better choice.
7. Accessibility: thinking keyboard, touch and scroll snap together
Unlike many JS carousels, CSS Scroll Snap supports keyboard navigation automatically, as soon as the container is focusable. With tabindex="0" on the scroll container, users can navigate through cards with arrow keys, and screen readers announce the container as a scrollable region. This is an area where many hand-built JS carousels fall short, since keyboard control is often missing entirely.
It still matters that every card in the carousel keeps its own focus order, for links or buttons inside it, and that prefers-reduced-motion is respected. Users who prefer reduced motion should not be forced into hard scroll-behavior: smooth, but should get a media query that switches to instant jumps when needed.
@media (prefers-reduced-motion: reduce) {
.carousel { scroll-behavior: auto; }
}
8. Browser support and fallback strategies
Scroll Snap is supported by all current browsers, including Safari, Chrome, Firefox and Edge, on both desktop and mobile. The feature also degrades very gracefully: a browser without support simply ignores the snap properties and still renders a normally scrollable container, without losing the carousel's core function. Dedicated fallback code is practically never necessary.
The one thing worth checking is the interaction with position: sticky elements inside the snap container, which historically showed quirks in some Safari versions. For complex layouts with nested sticky and snap elements, a manual test on real devices is worthwhile before rolling out such a combination to production.
9. When CSS Scroll Snap is enough and when a library still makes sense
For the vast majority of carousel and gallery use cases in e-commerce, from product images to testimonial sliders to category tiles, CSS Scroll Snap is entirely sufficient and saves an entire dependency. A JS library only remains worthwhile where features are needed that CSS fundamentally cannot express, such as infinite looping without a visible jump or synchronized multi-carousels sharing state.
| Property | Where set | Key values | Typical use |
|---|---|---|---|
scroll-snap-type |
Container | x/y/both mandatory/proximity |
Defining axis and strictness of snapping |
scroll-snap-align |
Child element | start/center/end |
Defining the snap point of each card |
scroll-padding |
Container | Length value | Safety margin from sticky headers |
scroll-margin |
Child element | Length value | Setting an individual snap offset per card |
| JS library | external | tool-dependent | Only for looping or synchronized multi-carousels |
Mironsoft
Modern CSS, layout architecture and rendering performance
CSS that stays maintainable instead of breaking with every change?
We review existing stylesheets for specificity chaos and layout thrashing, then build a CSS architecture with cascade layers, custom properties and modern layout primitives that still makes sense after the tenth feature.
CSS Audit
Systematically uncovering specificity issues, cascade conflicts and unused selectors.
Architecture Refactoring
Introducing cascade layers, custom properties and design tokens cleanly.
Performance Tuning
Fixing layout thrashing, expensive selectors and rendering bottlenecks.
10. Summary
CSS Scroll Snap: The Essentials at a Glance
Core idea
scroll-snap-type on the container plus scroll-snap-align on every child fully replace JS carousel libraries.
mandatory vs. proximity
mandatory for product carousels with fully visible cards, proximity for long, freely scrollable galleries.
Sticky header fix
scroll-padding on the container and scroll-margin on the child correct snap points behind fixed elements.
Accessibility
Keyboard navigation works automatically with tabindex, prefers-reduced-motion should specifically disable scroll-behavior.