Skip to content

Scroll Snap

CSS Scroll Snap provides native scroll snapping behavior, eliminating the need for heavy JavaScript carousel libraries.

<div class="carousel" id="carousel">
<div class="carousel-track">
<div class="slide">Slide 1</div>
<div class="slide">Slide 2</div>
<div class="slide">Slide 3</div>
</div>
<button class="prev"></button>
<button class="next"></button>
</div>
// JavaScript carousel implementation
class Carousel {
constructor(element) {
this.carousel = element;
this.track = element.querySelector('.carousel-track');
this.slides = element.querySelectorAll('.slide');
this.currentIndex = 0;
element.querySelector('.prev').addEventListener('click', () => this.prev());
element.querySelector('.next').addEventListener('click', () => this.next());
// Touch handling
let startX;
this.track.addEventListener('touchstart', (e) => {
startX = e.touches[0].clientX;
});
this.track.addEventListener('touchend', (e) => {
const diff = startX - e.changedTouches[0].clientX;
if (Math.abs(diff) > 50) {
diff > 0 ? this.next() : this.prev();
}
});
}
goTo(index) {
this.currentIndex = Math.max(0, Math.min(index, this.slides.length - 1));
this.track.style.transform = `translateX(-${this.currentIndex * 100}%)`;
}
prev() {
this.goTo(this.currentIndex - 1);
}
next() {
this.goTo(this.currentIndex + 1);
}
}
new Carousel(document.getElementById('carousel'));

Problems:

  • Large JavaScript bundle for carousel library
  • Complex touch event handling
  • Manual scroll position calculations
  • Accessibility often overlooked
  • Performance issues with many slides
  • Different libraries, different APIs
.carousel {
display: flex;
overflow-x: auto;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
}
.slide {
flex: 0 0 100%;
scroll-snap-align: start;
}
/* Hide scrollbar but keep functionality */
.carousel {
scrollbar-width: none;
}
.carousel::-webkit-scrollbar {
display: none;
}
<!-- Simple HTML - no JavaScript needed for basic functionality -->
<div class="carousel">
<div class="slide">Slide 1</div>
<div class="slide">Slide 2</div>
<div class="slide">Slide 3</div>
</div>

Benefits:

  • Zero JavaScript for basic carousel
  • Native touch and mouse scrolling
  • Built-in momentum scrolling
  • Keyboard accessible (arrow keys)
  • Better performance
  • Smaller bundle size
.container {
/* Required: Enable scroll snapping */
scroll-snap-type: x mandatory;
/* or */
scroll-snap-type: y proximity;
/* or */
scroll-snap-type: both mandatory;
/* Scroll padding - offset snap positions */
scroll-padding: 20px;
scroll-padding-inline: 50px;
}
ValueBehavior
mandatoryAlways snaps to nearest snap point
proximitySnaps when close to a snap point
xHorizontal scrolling
yVertical scrolling
bothBoth directions
.item {
/* Where this element snaps */
scroll-snap-align: start;
/* or */
scroll-snap-align: center;
/* or */
scroll-snap-align: end;
/* Prevent snapping past this element */
scroll-snap-stop: always;
}
.carousel {
display: flex;
gap: 16px;
overflow-x: auto;
scroll-snap-type: x mandatory;
scroll-padding-inline: 24px;
padding: 24px;
}
.carousel-item {
flex: 0 0 300px;
scroll-snap-align: start;
}
/* Show multiple items */
.carousel-item {
flex: 0 0 calc(33.333% - 12px);
}
html {
scroll-snap-type: y mandatory;
}
section {
height: 100vh;
scroll-snap-align: start;
scroll-snap-stop: always;
}
.gallery {
display: flex;
overflow-x: auto;
scroll-snap-type: x mandatory;
}
.gallery img {
flex: 0 0 100%;
height: 60vh;
object-fit: contain;
scroll-snap-align: center;
}
.card-carousel {
display: flex;
gap: 16px;
overflow-x: auto;
scroll-snap-type: x mandatory;
padding: 20px;
/* Show next card peeking */
scroll-padding-inline-start: 20px;
}
.card {
flex: 0 0 calc(100% - 60px);
scroll-snap-align: start;
}
@media (min-width: 768px) {
.card {
flex: 0 0 calc(50% - 28px);
}
}
.story-container {
height: 100vh;
overflow-y: auto;
scroll-snap-type: y mandatory;
}
.story {
height: 100vh;
scroll-snap-align: start;
scroll-snap-stop: always;
}
.tabs-content {
display: flex;
overflow-x: hidden;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
}
.tab-panel {
flex: 0 0 100%;
scroll-snap-align: start;
}
/* Navigate via anchor links */
/* <a href="#panel-2">Tab 2</a> */
.timeline {
display: flex;
gap: 32px;
overflow-x: auto;
scroll-snap-type: x proximity;
padding: 40px 20px;
}
.timeline-event {
flex: 0 0 250px;
scroll-snap-align: center;
}
// Optional: Dot indicators and prev/next buttons
const carousel = document.querySelector('.carousel');
const items = carousel.querySelectorAll('.carousel-item');
// Scroll to specific item
function goToSlide(index) {
items[index].scrollIntoView({
behavior: 'smooth',
inline: 'start',
block: 'nearest',
});
}
// Detect current slide
carousel.addEventListener('scrollend', () => {
const scrollLeft = carousel.scrollLeft;
const itemWidth = items[0].offsetWidth;
const currentIndex = Math.round(scrollLeft / itemWidth);
// Update indicators...
});

Supported in all modern browsers. See caniuse.com/css-snappoints.

scroll-snap-stop has good support. See caniuse.com/mdn-css_properties_scroll-snap-stop.

  • Native implementation is highly optimized
  • Uses GPU acceleration for smooth scrolling
  • No JavaScript execution during scroll
  • Better battery life on mobile devices
  • Works with virtualized lists

Use Scroll Snap when:

  • Building image carousels or galleries
  • Creating horizontal scrolling sections
  • Implementing full-page scroll experiences
  • Building mobile-friendly card lists
  • Creating tabbed content with swipe navigation

Add JavaScript when:

  • You need dot indicators or navigation buttons
  • Tracking current slide index
  • Auto-play functionality
  • Complex navigation requirements