CSS scroll-snap in Practice —
Carousel-Like UIs Without JavaScript

You don't need Swiper, Slick, or any JS library to make scroll positions snap into place. CSS scroll-snap gives you that satisfying "click-into-position" feel with just a few properties.

What Is scroll-snap?

Whenever I needed a carousel in the past, my first move was reaching for a JavaScript library. Swiper, Flickity, Slick — all solid tools. But for a lot of cases, what I actually wanted was simple: let the user scroll horizontally, and make items snap neatly into position. Loading 30KB+ of JavaScript for that always felt like overkill.

CSS scroll-snap solves exactly this problem. You tell the scroll container which direction to snap and how strictly, then mark each child element as a snap target. The browser handles all the physics — the deceleration, the touch momentum, the final snap animation. You write layout code, and the browser gives you native-feeling interaction for free.

Browser support is no longer a concern either. Every modern browser — Chrome, Firefox, Safari, Edge — has supported scroll-snap since 2019 at least. It's production-ready and has been for years.

📌 Key Point
scroll-snap controls where scrolling comes to rest. The browser handles the animation, momentum, and touch physics. You just define the layout and snap positions.

The Two-Step Setup: Parent + Child

The mental model for scroll-snap is refreshingly simple. There are two properties to learn, and they go on two different elements.

Parent: scroll-snap-type

On the scroll container, you set scroll-snap-type. It takes a direction (x or y) and a strictness value (mandatory or proximity). mandatory means "always land on a snap point." proximity means "snap if you're close enough, otherwise keep going."

CSS for the parent container
.scroll-container {
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  display: flex;
  gap: 16px;
}

Child: scroll-snap-align

On each child, you set scroll-snap-align to define which edge should align with the container's snap position. The options are start (left/top edge), center, and end (right/bottom edge).

CSS for each child item
.scroll-item {
  scroll-snap-align: start;
  flex-shrink: 0;
  width: 280px;
}

That's it. Two properties, two elements. Let's see it in action.

▶ Live Demo — Basic Horizontal Scroll Snap
1
2
3
4
5
6

← Scroll horizontally. Each item snaps to its left edge.

mandatory vs. proximity — When to Use Which

The first decision you'll face with scroll-snap is choosing between mandatory and proximity. The answer depends on what kind of UI you're building.

✅ mandatory — Best for full-screen scroll and carousels
.container {
  scroll-snap-type: y mandatory;
  /* Always lands on a snap point */
  /* Even if the user tries to stop between items, it pulls them back */
}
✅ proximity — Best for galleries and long lists
.container {
  scroll-snap-type: x proximity;
  /* Snaps if you're close to a point */
  /* Fast flicks pass through freely */
}

The key thing to remember: mandatory takes away scrolling freedom. For a landing page with one section per screen, that's exactly what you want. For a product carousel with twenty cards, it can feel frustrating — the user wants to quickly scan ahead, but the snap keeps pulling them back.

💡 Pro Tip
When in doubt, start with mandatory and test it on a phone. If a fast swipe feels annoying — like the page is fighting you — switch to proximity. That simple test is usually all you need.
▶ Live Demo — mandatory vs. proximity
mandatory (always snaps)
Card 1
Card 2
Card 3
Card 4
Card 5
Card 6
Card 7
Card 8
proximity (snaps when close)
Card 1
Card 2
Card 3
Card 4
Card 5
Card 6
Card 7
Card 8

Top row (mandatory): always snaps to a card edge. Bottom row (proximity): a fast swipe flies right past.

Three Patterns You'll Actually Use

Pattern 1: Card Carousel with scroll-padding

Here's a scenario I run into all the time: the design has side margins on the page, but the carousel should bleed edge-to-edge while still snapping with an offset. The solution is scroll-padding. It shifts the snap alignment point inward without affecting the layout of the children.

Offset snapping with scroll-padding
.carousel {
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-padding-left: 24px;
  display: flex;
  gap: 16px;
  padding: 0 24px;
}

.carousel-card {
  scroll-snap-align: start;
  flex-shrink: 0;
  width: 280px;
}
▶ Live Demo — Carousel with scroll-padding

Notice the 24px left offset. Cards snap at that offset, not at the container edge.

Pattern 2: Full-Screen Vertical Scroll

Landing pages and presentation-style sites that advance one screen at a time used to require libraries like fullPage.js. With scroll-snap, the CSS is almost trivially simple.

Full-screen vertical snap
.fullpage {
  height: 100vh;
  overflow-y: auto;
  scroll-snap-type: y mandatory;
}

.fullpage-section {
  height: 100vh;
  scroll-snap-align: start;
  display: flex;
  align-items: center;
  justify-content: center;
}
▶ Live Demo — Full-Screen-Style Vertical Snap
Section 1↓ Scroll down
Section 2Snaps into place
Section 3CSS only — no libraries
Section 4fullPage.js not required

This demo uses a 300px container. In production, you'd use height: 100vh for true full-screen.

Pattern 3: Center Snap for Focus UIs

scroll-snap-align: center keeps the selected item centered in the viewport. It's perfect for image galleries or picker-style UIs where you want to highlight the current selection.

Center snap configuration
.gallery {
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  display: flex;
  gap: 12px;
  padding: 0 calc(50% - 100px);
  /* Ensures first/last items can also reach center */
}

.gallery-item {
  scroll-snap-align: center;
  flex-shrink: 0;
  width: 200px;
}
▶ Live Demo — Center Snap Gallery
A
B
C
D
E

Items always snap to center. The padding trick ensures even the first and last items can reach center position.

⚠️ Watch Out
For center snapping to work on the first and last items, you need to add horizontal padding to the container equal to calc(50% - half-item-width). Without it, edge items will snap off-center, and you'll spend twenty minutes wondering what went wrong.

Preventing Skip-Ahead with scroll-snap-stop

Even with mandatory, a fast flick can skip over multiple snap points. If you need every single item to be seen — think onboarding steps or a tutorial — add scroll-snap-stop: always to the children.

Force stopping at every item
.card {
  scroll-snap-align: start;
  scroll-snap-stop: always;
  /* Even a fast flick stops at each card */
}

Use this sparingly. scroll-snap-stop: always severely limits the user's ability to browse quickly. It makes sense for onboarding flows or legal-disclosure screens. For a product carousel? Your users will feel trapped.

💡 Pro Tip
"I want users to see every card" is a designer's wish, not a user's wish. In product carousels and galleries, skip scroll-snap-stop entirely. Reserve it for content that genuinely can't be skipped — onboarding steps, safety disclaimers, multi-step forms.

Responsive Design and Accessibility

Making scroll-snap responsive is straightforward. Set card widths in viewport units or percentages, and the snap behavior naturally adapts. For a common pattern — single card on mobile, three across on desktop — you'd write something like this:

Responsive card widths
.card {
  scroll-snap-align: start;
  flex-shrink: 0;
  width: 85vw;  /* Mobile: near full-width */
}

@media (min-width: 768px) {
  .card {
    width: calc(33.333% - 12px);  /* Desktop: 3 columns */
  }
}

On the accessibility side, scroll-snap works with keyboard navigation (Tab + arrow keys) out of the box. To make the experience clear for screen reader users, add tabindex="0", role="region", and aria-label to the scroll container.

Accessibility attributes
<div
  class="carousel"
  role="region"
  aria-label="Project gallery"
  tabindex="0"
>
  <!-- cards -->
</div>

Takeaways

  • scroll-snap requires just two steps: scroll-snap-type on the parent, scroll-snap-align on the children
  • mandatory always snaps; proximity snaps only when close — test on mobile to decide which feels right
  • scroll-padding shifts the snap alignment point without changing the layout — perfect for offset carousels
  • scroll-snap-align: center creates focus UIs where the active item is always centered
  • scroll-snap-stop: always prevents skip-ahead but limits user freedom — use it only where skipping would be a problem
  • Responsive behavior comes from setting card widths in vw or %, and scroll-snap adapts automatically
  • Add role="region" and aria-label to ensure your scroll containers are accessible to screen readers