1001Ferramentas
πŸ“œGenerators

CSS Scroll Snap

Gera scroll-snap-type horizontal/vertical.

CSS

β€”

CSS scroll-snap: native carousels without JavaScript

The scroll-snap CSS module lets you control where the browser settles when a scroll gesture ends β€” instead of stopping at an arbitrary pixel, the viewport "snaps" to predefined points along the scroll axis. It replaces a whole family of JS carousel libraries (Slick, parts of Swiper.js, Glide.js) for the common case of a horizontal slider or a TikTok/Reels-style vertical feed. Because the snap logic runs in the compositor thread, momentum, fling and inertia all behave natively β€” no JS reflow during scroll, no missed touch events, and keyboard arrow keys work for free.

Browser support is universal on modern engines: Chrome 69+, Firefox 68+, Safari 11+ (Edge inherits from Chromium). Mobile Safari handles it especially well because it integrates with the platform's touch momentum.

The two-side contract: parent and child

Scroll-snap always involves two properties on two elements. The scroll container declares the snap axis and strictness; each child declares where it wants to align.

.carousel {
  scroll-snap-type: x mandatory;
  overflow-x: auto;
  scroll-behavior: smooth;
}
.carousel > .slide {
  scroll-snap-align: start;
}

scroll-snap-type accepts x | y | both for the axis and mandatory | proximity for the strictness: mandatory always snaps to the nearest point, even if the user only scrolled one pixel; proximity only snaps when already close, which feels more forgiving for reading content. On the child, scroll-snap-align is start, center or end β€” the edge of the child that aligns with the same edge of the container.

scroll-padding and sticky headers

If your page has a fixed/sticky header, snap points hide under it. Use scroll-padding-top: 60px on the container to push the snap origin down by the header height β€” children align below the bar instead of behind it. The shorthand scroll-padding mirrors the regular padding sides, and scroll-margin does the same job from the child side. Pair these with scroll-snap-stop: always to prevent fast flicks from skipping intermediate slides β€” useful for onboarding wizards or step-by-step product tours.

Pagination dots and active state

scroll-snap doesn't expose a "current slide" event natively. The idiomatic way to light up pagination dots is an IntersectionObserver with threshold: 0.5 watching each slide and toggling an aria-current="true" attribute on the matching dot. If you need a callback exactly when the snap settles, listen to the new scrollend event (Chrome 114+, Firefox 109+) β€” earlier browsers need a debounced scroll listener.

Real-world examples

  • Apple product pages β€” horizontal feature carousels with mandatory snap.
  • Stripe.com β€” case-study sliders that pause crisply on each customer logo.
  • TikTok / Instagram Reels web β€” full-viewport vertical snap, one video at a time.
  • Image galleries β€” thumbnail rows with scroll-snap-align: center for a Netflix-row feel.

FAQ

Can I replace Swiper.js entirely? For a basic horizontal slider with pagination, yes β€” scroll-snap plus a small IntersectionObserver beats Swiper on bundle size and performance. For advanced features (loop, virtual slides, parallax, thumbnail sync) Swiper still wins.

How do I intercept the snap event? Use the scrollend event when available, otherwise debounce scroll by ~150ms after the last fire. Compute the active index from container.scrollLeft / container.clientWidth.

Why do nested scroll containers misbehave? When a snapping container sits inside another scrollable parent, the browser has to decide which one captures the gesture. Set overscroll-behavior: contain on the inner container so flicks don't leak to the outer scroll.

mandatory or proximity? Use mandatory for image carousels and discrete cards (you want every snap to land). Use proximity for long-form content like a magazine where the user is reading mid-paragraph and shouldn't be yanked to the next section.

Related Tools