Problem Solved

CSS · scroll-snap

Home › Courses › CSS › Problems › scroll-snap
🎨 Concept · 3 min read

CSS scroll-snap.
Carousels without JavaScript.

Two properties, no JavaScript, native scroll physics. scroll-snap-type on the container defines the axis and behavior. scroll-snap-align on the children defines where they snap. The result: a carousel that feels native on every device, uses the browser's scroll momentum, and requires no event listeners, no drag handling, no third-party library. This page covers the syntax, the mandatory vs proximity choice, and the accessibility patterns that make it safe.

Short answer: On the container: overflow-x: auto; scroll-snap-type: x mandatory;. On each child: scroll-snap-align: start;. That's it — a horizontal carousel with native snap. Use mandatory for carousels and full-page scroll; use proximity when you want free scroll near snap points. Hide the scrollbar only if you provide alternative navigation (arrows, dots). Never use scroll-snap-type: y mandatory on a normal page — it traps users who want to scroll freely.

✅ The two properties

Container and children. That's all.

scroll-snap-type (container)

Defines the axis (x, y, both) and behavior (mandatory, proximity). Set on the scroll container.

scroll-snap-align (children)

Where each child snaps: start, center, end. Set on each direct child of the container.

scroll-padding (container)

Offset for the snap position — useful when there's a fixed header. scroll-padding-top: 80px.

scroll-behavior (container)

Optional. smooth for animated scrolling on anchor links and programmatic scroll.

✅ Before and after

A horizontal carousel in CSS.

carousel.css
.carousel {
display: flex;
gap: 1rem;
overflow-x: auto;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
scrollbar-width: none; /* Firefox */
}

.carousel::-webkit-scrollbar {
display: none; /* WebKit */
}

.slide {
flex: 0 0 100%; /* each slide = full width /
scroll-snap-align: start;
height: 300px;
background: #2965f1;
border-radius: 8px;
}
User swipes right:
┌─────────────┐
│ Slide 1 │ → snaps to start
└─────────────┘
Swipe more:
┌─────────────┐
│ Slide 2 │ → snaps to start
└─────────────┘

✅ Each slide is full width (flex: 0 0 100%).
✅ scroll-snap-align: start makes each slide
snap to the left edge of the container.
✅ scroll-snap-type: x mandatory forces the
browser to always land on a slide edge.
✅ Native momentum on touch devices.
✅ Works with mouse wheel, trackpad, keyboard arrows.

gap: 1rem adds space between slides.
flex: 0 0 100% means "don't shrink, don't grow,
base size 100% of container". This is the
correct flex shorthand — flex: 1 would let
the slide grow, breaking the layout.

scrollbar-width: none hides the scrollbar.
::--webkit-scrollbar { display: none } does the same
for Chrome/Safari/Edge.
Hide only if you have alternative navigation.

✅ Mandatory vs proximity

How aggressive is the snap.

mandatory-vs-proximity.css
/* mandatory — always snaps to a point */
.carousel-mandatory {
display: flex;
overflow-x: auto;
scroll-snap-type: x mandatory;
}

.carousel-mandatory .slide {
flex: 0 0 100%;
scroll-snap-align: start;
}

/* proximity — snaps only when close */
.gallery-proximity {
display: flex;
gap: 1rem;
overflow-x: auto;
scroll-snap-type: x proximity;
}

.gallery-proximity img {
flex: 0 0 300px;
scroll-snap-align: center;
}
mandatory:
✅ Every scroll ends exactly on a snap point.
✅ Precise — always shows a full slide.
❌ Can trap users — can't stop between slides.
✅ Best for: full-width carousels, full-page scroll.

proximity:
✅ Snaps when close, scrolls freely otherwise.
✅ Less aggressive — user feels in control.
❌ May not snap at all if scroll is far from a point.
✅ Best for: image galleries, mixed content,
carousels where users might want partial scroll.

Rule of thumb:
If every slide is the full width of the container
→ mandatory.
If slides are varied widths or you want free scroll
→ proximity.

Edge case — tabnavigation:
Users navigating with keyboard arrows (not Tab)
can scroll past proximity snap points freely.
With mandatory, they snap to each slide.
This is usually what you want.

⚠️ mandatory on a page with tall sections (y axis)
can trap users who want to scroll freely.
Only use scroll-snap-type: y mandatory on
full-page scroll designs, not on content pages.

✅ Accessibility — the non-negotiables

What makes a snap carousel usable for everyone.

accessible-snap.css
.carousel {
display: flex;
overflow-x: auto;
scroll-snap-type: x proximity;
scroll-behavior: smooth;

/* Respect reduced-motion */
scroll-behavior: auto;
}

@media (prefers-reduced-motion: no-preference) {
.carousel {
scroll-behavior: smooth;
}
}

/* Keyboard-visible focus for carousel controls */
.carousel-nav:focus-visible {
outline: 2px solid #2965f1;
outline-offset: 2px;
}

/* Visible navigation arrows — alternative to scrollbar */
.carousel-nav {
position: absolute;
top: 50%;
transform: translateY(-50%);
background: white;
border: 1px solid #cbd5e1;
border-radius: 50%;
width: 40px;
height: 40px;
}
Accessibility checklist for snap carousels:

✅ Prefer proximity over mandatory when possible.
Mandatory can trap users between slides.

✅ Respect prefers-reduced-motion:
• smooth scroll only if user hasn't requested reduced motion.
• remove any auto-play behavior entirely.

✅ Provide keyboard navigation:
• focus visible on all controls.
• keyboard arrow keys work natively.

✅ Provide non-scroll navigation:
• visible arrows (prev/next).
• dot indicators showing current slide.
• "skip carousel" link if it's a big component.

✅ Don't hide the scrollbar without alternatives.
If hidden, arrows/dots must be visible.

✅ Test with keyboard only, and with a screen reader.
Tab through the carousel — does focus move correctly?
Arrow keys — does the carousel respond?

✅ Announce state changes:
screen readers should know when the slide changes.
(JS required for this — beyond scroll-snap's scope.)

Don't use scroll-snap-type: y mandatory on a page with content sections. Full-page snap looks great in demos but traps users — they can't stop between sections to read, and they can't scroll to see part of a section. This is especially bad on mobile where users expect free scroll. Use mandatory snap only when each snap target is exactly the viewport (a slide deck, a full-page scroll design). For content pages, use no snap at all, or proximity at most. The rule: snap is for carousels and slide decks, not for normal reading.

⚠️ Common mistakes

The scroll-snap mistakes that cost real time.

1. scroll-snap-type on children

Must be on the container, not the slides. Fix: container has scroll-snap-type, children have scroll-snap-align.

2. Mandatory on a content page

Traps users between sections. Fix: use proximity, or no snap.

3. Hidden scrollbar with no alternative

Users don't know there's more content. Fix: add visible arrows or dots.

4. Forgetting overflow-x: auto

Snap needs a scroll container. Fix: overflow-x: auto on the container.

5. Slides without flex basis

Slides shrink to their content. Fix: flex: 0 0 100% (or an explicit width).

6. No gap between slides

Adjacent slides visually merge. Fix: gap on the container.

7. Forgetting scroll-padding

Snap points hide under fixed headers. Fix: scroll-padding-top: 80px.

8. Ignoring prefers-reduced-motion

Smooth scroll animates for users who requested none. Fix: wrap in @media (prefers-reduced-motion: no-preference).

❓ Frequently asked

What is CSS scroll-snap?

scroll-snap is a CSS feature that lets scrollable containers 'snap' to specific positions — a carousel that snaps to each slide, a full-page scroll that snaps to each section. Two properties: scroll-snap-type on the container (axis and behavior) and scroll-snap-align on the children (where they snap). No JavaScript required. Supported in all modern browsers since 2019.

What is the difference between scroll-snap-type mandatory and proximity?

mandatory always snaps to the nearest snap point — the scroll position ends up exactly on a snap point, always. proximity snaps only if the scroll is close enough to a snap point — otherwise it scrolls freely. mandatory gives precise snapping but can trap users. proximity is less aggressive. Use mandatory for carousels and full-page scroll; use proximity for image galleries.

How do I create a horizontal carousel with scroll-snap?

Three steps. (1) Container: display: flex; overflow-x: auto; scroll-snap-type: x mandatory. (2) Children: flex: 0 0 100%; scroll-snap-align: start. (3) Optional: scroll-behavior: smooth. This gives a horizontal carousel where each slide takes the full width and snaps into place. No JavaScript.

Does scroll-snap work on touch devices?

Yes — it's designed for touch. Swiping on a carousel with scroll-snap-type: x mandatory snaps to the nearest slide. The behavior feels native on iOS and Android, and it's often better than a JavaScript carousel because it uses the browser's native scroll momentum and physics.

Can I hide the scrollbar on a scroll-snap container?

Yes, with scrollbar-width: none (Firefox) and ::-webkit-scrollbar { display: none } (WebKit). But hiding the scrollbar removes a visual cue that the content is scrollable — users may not realize there are more slides. If you hide the scrollbar, provide alternative cues: visible arrows, dot indicators, or a partial next slide showing at the edge.

Does scroll-snap hurt accessibility?

It can, in two ways. (1) mandatory snap can trap users — they can't scroll past a slide if the browser insists on snapping. (2) Hidden scrollbars remove the visual cue of scrollability. Fixes: use proximity instead of mandatory for carousels where users might want free scroll; always provide keyboard navigation; never hide the scrollbar on vertical scrolling; provide visible navigation.

🔗 Related

← All CSS problems
Problem solved · CSS