CSS Scroll Snap lets you control where a scroll container stops — snapping to specific child elements after the user stops scrolling. It replaces JavaScript-based scroll-to logic for carousels, image galleries, and onboarding wizards with a few lines of CSS.
Problem: Implementing carousels and horizontally paginated layouts in WordPress themes traditionally requires a JavaScript library or custom scroll event handling — adding weight and complexity for a layout that CSS can handle natively.
Solution: Use CSS Scroll Snap — set scroll-snap-type: x mandatory on the container and scroll-snap-align: start on each item. Combined with overflow-x: scroll and hidden scrollbar styling, this creates a native carousel or paginated layout with no JavaScript. Add scroll-behavior: smooth for animated programmatic scrolling.
The examples below build a horizontal product card scroller with snap points, a full-page vertical scroll experience, and a paginated gallery with keyboard navigation support.
/* ── SCROLL CONTAINER ── */
.products-scroller {
display: flex;
gap: 1rem;
overflow-x: auto;
overflow-y: hidden;
/* Enable scroll snapping — mandatory: always snaps to an alignment point */
scroll-snap-type: x mandatory;
/* proximity: only snaps when near an alignment point (softer feel) */
/* scroll-snap-type: x proximity; */
/* Prevent nested scroll containers from stealing the scroll */
overscroll-behavior-x: contain;
/* Smooth scrolling for programmatic scrollTo() */
scroll-behavior: smooth;
/* Hide scrollbar but keep functionality */
scrollbar-width: none; /* Firefox */
&::-webkit-scrollbar { display: none; } /* Chrome/Safari */
}
/* ── SCROLL CHILDREN ── */
.product-card {
flex: 0 0 280px; /* fixed width prevents card from shrinking */
scroll-snap-align: start; /* snap left edge to container left edge */
/* stop: always pause at this snap point even if swiping quickly */
/* scroll-snap-stop: always; */ /* default is 'normal' (can skip when fast) */
}
/* ── FULL-PAGE VERTICAL SCROLL ── */
.sections-wrapper {
height: 100vh;
overflow-y: scroll;
scroll-snap-type: y mandatory;
}
.section {
height: 100vh;
scroll-snap-align: start;
scroll-snap-stop: always; /* prevent jumping two sections on fast swipe */
}
/* ── CENTERED SNAP (gallery style) ── */
.gallery {
display: flex;
overflow-x: auto;
scroll-snap-type: x mandatory;
scroll-padding-inline: 1rem; /* offset snap position from container edge */
}
.gallery img {
scroll-snap-align: center; /* snap image centre to container centre */
flex: 0 0 auto;
width: min(90vw, 600px);
}
Add prev/next buttons with JavaScript:
// Programmatic snap navigation — scroll to the next snap point
const scroller = document.querySelector('.products-scroller');
const cards = scroller.querySelectorAll('.product-card');
let currentIndex = 0;
document.querySelector('.btn-next').addEventListener('click', () => {
currentIndex = Math.min(currentIndex + 1, cards.length - 1);
cards[currentIndex].scrollIntoView({ behavior: 'smooth', inline: 'start' });
});
document.querySelector('.btn-prev').addEventListener('click', () => {
currentIndex = Math.max(currentIndex - 1, 0);
cards[currentIndex].scrollIntoView({ behavior: 'smooth', inline: 'start' });
});
// Detect which card is currently snapped using IntersectionObserver
const observer = new IntersectionObserver(
entries => {
entries.forEach(entry => {
if ( entry.isIntersecting ) {
currentIndex = [...cards].indexOf(entry.target);
updateDots(currentIndex); // update pagination dots UI
}
});
},
{ root: scroller, threshold: 0.6 }
);
NOTE: CSS Scroll Snap is supported in all modern browsers. Use scroll-snap-type: x mandatory for carousels where you always want exactly one item visible. Use scroll-padding on the container (not the children) to account for sticky headers — if your site has a fixed 60px nav bar, add scroll-padding-top: 60px to the scroll container so snap points appear below the nav.