CSS Scroll Snap: Carousels and Paginated Layouts Without JavaScript

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.

Leave Comment

Your email address will not be published. Required fields are marked *