Scroll Snap & Carousels
Many "carousels" are a horizontal scroller with snap points. Scroll snap gives you that behaviour in CSS -- accessible keyboard scrolling included -- without a JavaScript slider library for the basic case.
Core API
The scroller
.scroller {
display: flex;
gap: 1rem;
overflow-x: auto;
scroll-snap-type: x mandatory;
scroll-padding-inline: 1rem;
-webkit-overflow-scrolling: touch;
}
| Property | Role |
|---|---|
scroll-snap-type | Axis (x / y / both / inline / block) + strictness (mandatory / proximity) |
scroll-padding-* | Insets so snapped items clear sticky headers or peek gutters |
overflow | Must allow scrolling on that axis |
Prefer proximity when mandatory snapping feels aggressive; use mandatory for full-bleed slides.
The items
.scroller > .slide {
flex: 0 0 85%;
scroll-snap-align: start;
scroll-snap-stop: always; /* optional: force stop on each slide */
}
| Property | Role |
|---|---|
scroll-snap-align | start / center / end -- which edge snaps |
scroll-snap-stop | normal or always -- whether fast flicks can skip past |
Pattern: card carousel
<div class="scroller" aria-label="Featured articles">
<article class="slide">…</article>
<article class="slide">…</article>
<article class="slide">…</article>
</div>
.scroller {
display: grid;
grid-auto-flow: column;
grid-auto-columns: min(20rem, 85%);
gap: 1rem;
overflow-x: auto;
scroll-snap-type: inline mandatory;
scroll-padding-inline: 1rem;
padding-inline: 1rem;
}
.slide {
scroll-snap-align: start;
}
Grid auto-columns keep slide width consistent without Flex flex-basis math.
Pattern: full-viewport sections
.pages {
height: 100dvh;
overflow-y: auto;
scroll-snap-type: y mandatory;
}
.pages > section {
min-height: 100dvh;
scroll-snap-align: start;
}
Use sparingly -- full-page mandatory snap can trap users and fight browser find-in-page / accessibility tools. Offer a non-snapping alternative when content is long-form.
Controls without a library
Native scroll already supports trackpads, touch, and Shift+wheel in many browsers. Optional previous/next buttons:
prev.addEventListener('click', () => {
scroller.scrollBy({ left: -scroller.clientWidth, behavior: 'smooth' });
});
Keep buttons as enhancement; the scroller must work without them.
For dots / markers, newer CSS scroll-marker proposals are emerging -- treat as progressive enhancement and verify
Baseline status before depending on them. A small script that toggles aria-current on dots from scroll events is
still a solid approach.
Accessibility
- Provide a clear
aria-label(or labelled region) on the scroller. - Do not auto-advance slides by default; if you must, pause on hover/focus and respect
prefers-reduced-motion. - Ensure slide content is reachable by keyboard (Tab to links/buttons inside slides).
- Avoid
scroll-snap-type: … mandatoryon the document root for article reading UIs.
@media (prefers-reduced-motion: reduce) {
.scroller {
scroll-behavior: auto;
}
}
Pairing with other modern CSS
- Scroll-driven animations can fade captions as slides cross the scrollport.
- Container queries can change slide density inside a dashboard widget.
- Peek layouts (
flex-basis: 85%) hint that more content exists -- better than clipping with no affordance.
Gotchas
- Nested scrollers fight each other -- avoid snap on both parent and child unless intentional.
mandatory+ uneven widths can feel sticky; test with real content.- Focus. Focused elements scrolled into view may disagree with snap alignment -- test keyboard paths.
- Libraries. Reach for a JS carousel when you need infinite loops, complex lazy loading, or analytics-heavy slide APIs -- not for basic snap galleries.
Related reading
- Scroll-Driven Animations
- Practice Projects -- carousel practice idea
- MDN: CSS scroll snap
- Inspiration: modern-css.com scroll snapping