Scroll Snap

Difficulty: Advanced

CSS Scroll Snap provides native, performant snapping behavior for scrollable containers - ensuring that scrolling always lands on specific snap points rather than stopping at random positions. This creates the polished, app-like scrolling experience seen in image carousels, full-page scroll sections, card galleries, and mobile-style swipe interfaces, all without JavaScript libraries or custom scroll event handlers.

The scroll-snap-type property is applied to the scroll container (the element with overflow: scroll or overflow: auto). It takes two values: the axis (x for horizontal, y for vertical, both for both, or block/inline for logical axes) and the strictness (mandatory or proximity). This activates scroll snapping on the container, making it a scroll snap container. The children inside it are the potential snap targets.

The mandatory vs proximity distinction is critical. With mandatory snapping, the scroll container always snaps to the nearest snap point after any scroll action - even a tiny 1px scroll will cause the view to jump to the next section. This is ideal for full-page sections and image carousels where you always want exactly one item visible. With proximity snapping, the container only snaps if the scroll position is close enough to a snap point - if the user scrolls to a position between snap points that is not near either one, the scroll stops there naturally. Proximity is better for long content pages where snapping should feel optional and assistive.

The scroll-snap-align property is applied to the child elements (snap targets) within the scroll container. It specifies which part of the child should align with which part of the container: start aligns the start edges, center aligns the centers, and end aligns the end edges. For horizontal carousels, start is most common (each card's left edge aligns with the container's left edge). For full-page vertical sections, start or center works well.

Additional properties fine-tune the behavior. scroll-padding on the container creates inset from the container edges where snap points align - useful when a fixed header overlaps the snap area. scroll-margin on individual children adds offset around each snap target. scroll-snap-stop: always on a child prevents the user from skipping over that item during fast scrolling (the default normal allows flying past items). These properties together create polished scroll experiences that rival JavaScript-based solutions with zero runtime cost.

Code examples

Horizontal Card Carousel

/* Scroll container */
.carousel {
  display: flex;
  gap: 16px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-padding: 0 24px; /* Account for container padding */
  padding: 0 24px;

  /* Hide scrollbar (optional) */
  scrollbar-width: none;
}
.carousel::-webkit-scrollbar {
  display: none;
}

/* Snap targets */
.carousel-card {
  flex: 0 0 280px; /* Fixed width, no shrink */
  scroll-snap-align: start;
  border-radius: 12px;
  overflow: hidden;
  background: white;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.carousel-card img {
  width: 100%;
  height: 180px;
  object-fit: cover;
}

.carousel-card .content {
  padding: 16px;
}

<!-- HTML -->
<div class="carousel">
  <div class="carousel-card">
    <img src="img1.jpg" alt="Card 1" />
    <div class="content"><h3>Card 1</h3></div>
  </div>
  <div class="carousel-card">
    <img src="img2.jpg" alt="Card 2" />
    <div class="content"><h3>Card 2</h3></div>
  </div>
  <!-- More cards... -->
</div>

The container gets scroll-snap-type: x mandatory for horizontal snapping. Each card gets scroll-snap-align: start so the left edge of each card aligns with the left edge of the container. scroll-padding offsets the snap point to account for container padding.

Full-Page Vertical Sections

/* Full-page scroll container */
.page-container {
  height: 100vh;
  overflow-y: auto;
  scroll-snap-type: y mandatory;
}

/* Each section fills the viewport */
.section {
  height: 100vh;
  scroll-snap-align: start;
  display: flex;
  align-items: center;
  justify-content: center;
}

.section:nth-child(1) { background: #1a1a2e; color: white; }
.section:nth-child(2) { background: #16213e; color: white; }
.section:nth-child(3) { background: #0f3460; color: white; }
.section:nth-child(4) { background: #533483; color: white; }

/* Prevent skipping sections during fast scroll */
.section {
  scroll-snap-stop: always;
}

<!-- HTML -->
<div class="page-container">
  <section class="section"><h1>Section 1</h1></section>
  <section class="section"><h1>Section 2</h1></section>
  <section class="section"><h1>Section 3</h1></section>
  <section class="section"><h1>Section 4</h1></section>
</div>

y mandatory ensures every scroll ends exactly on a section boundary. Each section is 100vh so it fills the viewport. scroll-snap-stop: always prevents users from flying past sections during fast swipe gestures - each section must be visited.

Mandatory vs Proximity Snapping

/* Mandatory: ALWAYS snaps to nearest point */
.mandatory-container {
  overflow-y: auto;
  scroll-snap-type: y mandatory;
  height: 400px;
}

.mandatory-container .item {
  height: 400px; /* Same as container */
  scroll-snap-align: start;
}

/* Proximity: Only snaps when close to a snap point */
.proximity-container {
  overflow-y: auto;
  scroll-snap-type: y proximity;
  height: 400px;
}

.proximity-container .item {
  min-height: 300px; /* Variable heights */
  scroll-snap-align: start;
}

/* Proximity is better for content-heavy pages */
.article-sections {
  scroll-snap-type: y proximity;
}

.article-sections section {
  scroll-snap-align: start;
  /* Sections have varying heights based on content */
  /* Proximity allows natural scrolling between sections */
  /* Only snaps when scroll stops near a section boundary */
}

mandatory always snaps - best for fixed-size items like carousels and full-page sections. proximity only snaps when you stop near a snap point - best for variable-height content where forced snapping would feel jarring. Choose based on your content structure.

Scroll Padding and Snap Margins

/* scroll-padding on container: offset for sticky header */
.container {
  overflow-y: auto;
  scroll-snap-type: y mandatory;
  scroll-padding-top: 80px; /* Height of sticky header */
}

.sticky-header {
  position: sticky;
  top: 0;
  height: 80px;
  z-index: 10;
  background: white;
}

.section {
  scroll-snap-align: start;
  /* Snaps to 80px from the top, below the header */
}

/* scroll-margin on children: per-item offset */
.gallery-item {
  scroll-snap-align: center;
  scroll-margin: 16px; /* Adds space around this snap target */
}

/* Combining for a complete layout */
.page {
  overflow-y: auto;
  scroll-snap-type: y proximity;
  scroll-padding: 80px 0 0 0; /* Top padding for header */
}

.page section {
  scroll-snap-align: start;
  scroll-margin-top: 16px; /* Extra breathing room */
  min-height: 50vh;
  padding: 48px 24px;
}

/* Horizontal carousel with peek effect */
.peek-carousel {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-padding-inline: 32px;
}

.peek-carousel .card {
  flex: 0 0 85%; /* Show a peek of the next card */
  scroll-snap-align: center;
  scroll-margin-inline: 8px;
}

scroll-padding on the container offsets where snap points align - essential when a sticky header would overlap snapped content. scroll-margin on individual items adds per-element offset. For peek carousels, use smaller flex-basis with center alignment to show partial next/previous items.

Key points

Concepts covered

scroll-snap-type, scroll-snap-align, mandatory vs proximity, Scroll Containers, Scroll Padding