Web development
Carousels with scroll-snap: three properties and no JavaScript
scroll-snap turns an overflow container into a carousel that snaps, with no JavaScript. The three properties, the edge case and the keyboard check.
Key points
- Three properties are enough: scroll-snap-type on the container, scroll-snap-align on the child, scroll-padding for the first element.
- mandatory for cards of equal width, proximity when the cards differ.
- CSS gives you neither the label nor the counter: those you still write, and they matter.
Short answer: scroll-snap-type plus scroll-snap-align turn a container with overflow-x: auto into a carousel that snaps, with no JavaScript. Dragging stays native and the keyboard keeps working.
The three properties you need
| Property | Where | What it does |
|---|---|---|
scroll-snap-type: x mandatory | container | snaps on the horizontal axis, with no in-between |
scroll-snap-align: start | child | says where the element comes to rest |
scroll-padding-inline | container | accounts for the padding, so the first element is not clipped |
The code
.carousel {
display: flex;
gap: 1rem;
overflow-x: auto;
scroll-snap-type: x mandatory;
scroll-padding-inline: 1rem;
overscroll-behavior-inline: contain;
scrollbar-width: none;
}
.carousel > * {
flex: 0 0 min(80vw, 22rem);
scroll-snap-align: start;
}
@media (prefers-reduced-motion: reduce) {
.carousel { scroll-behavior: auto; }
}
overscroll-behavior-inline stops the gesture from carrying on into the page when the carousel reaches the end: without it, vertical scrolling jams on a phone. min(80vw, 22rem) gives a nearly full card on a phone and a fixed one on desktop.
mandatory or proximity
mandatory forces the snap and works for cards of equal width. When the cards differ in size the snap feels like a jump: proximity is gentler there, because it only snaps when you are already near the point.
Support has been broad since 2019. On dated Safari you still need -webkit-overflow-scrolling: touch for inertial scrolling, and that property must come before overflow.
The limitation
A scroll-snap carousel has no arrows, no counter and no label. A screen reader user cannot tell how many cards exist: the container needs an accessible label and a text indicator such as “3 of 8”. CSS alone does not give you either, and without them the carousel is an invisible horizontal list.
How to check it
- Drag with a trackpad, a touch screen and a keyboard: the snap must land on the same element.
- Check the tab order: it must follow the visual order, not the DOM order if you split them.
- Turn on
prefers-reduced-motionin the system: no animated scrolling. - Shrink the window to 320px: the first card must not be clipped by the padding.
Where to continue
In the catalogue: testimonial block, components and blocks. The guides: centring an element and a navbar on mobile; the comparison: Tailwind against CSS Modules.
Frequently asked questions
Do I still need a carousel library?
Not for scrolling. A library earns its place when you want autoplay, infinite loop or arrows with state, which scroll-snap does not do.
Why does the snap not work?
Almost always because the container has no overflow declared, or because the property sits on an ancestor. scroll-snap-type goes on the element that scrolls.
How do I hide the scrollbar?
With <code>scrollbar-width: none</code> and <code>::-webkit-scrollbar</code> for older engines. Remember that hiding the bar also removes the hint that there is more to scroll.
Does it work vertically?
Yes, with <code>scroll-snap-type: y</code>. On a whole page it is invasive though: I would only use it inside a panel with a fixed height.
Does the carousel hurt CLS?
No, if the cards declare a ratio. The risk is width, not height: a card with no measurement changes how many items are visible while it loads.