Articles /

layout

Scroll snap: CSS-native carousels

Scroll snapping gives users the satisfying "click into place" feel of native apps. Two properties on the container, one on each child.

The two-part API

Scroll snap requires properties on both the scroll container and its children. The container declares the snap axis and strictness; each child declares where it should align:

/* 1. The scroll container */
.carousel {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  gap: 1rem;
}

/* 2. Each snap target */
.carousel > .slide {
  scroll-snap-align: start;
  flex: 0 0 100%;
}

The scroll container must have an overflow value that creates a scrollport — auto, scroll, or hidden.

mandatory vs proximity

The scroll-snap-type strictness value controls how aggressively the browser snaps:

/* mandatory: always snaps to the nearest point */
/* User cannot rest between snap points */
.carousel { scroll-snap-type: x mandatory; }

/* proximity: snaps only when close to a snap point */
/* User can stop anywhere — snapping is a suggestion */
.gallery { scroll-snap-type: y proximity; }

/* Both axes */
.grid-scroll { scroll-snap-type: both mandatory; }
Use mandatory for full-page sections and carousels where partial views don't make sense. Use proximity for long lists where the user needs freedom to scroll naturally.

Snap alignment options

The scroll-snap-align property controls which edge of the child aligns with the container's snap position:

/* Align the start edge of each child with the container's start */
.slide { scroll-snap-align: start; }

/* Center each child in the viewport */
.slide { scroll-snap-align: center; }

/* Align the end edge */
.slide { scroll-snap-align: end; }

/* Different alignment per axis: block inline */
.slide { scroll-snap-align: start center; }

/* Remove a child from snapping */
.no-snap { scroll-snap-align: none; }

Scroll padding and margin

Use scroll-padding on the container to offset snap positions (for example, to account for a sticky header). Use scroll-margin on individual children to adjust their snap offset:

/* Container: offset snap point below sticky header */
.carousel {
  scroll-snap-type: x mandatory;
  scroll-padding-inline: 2rem;
}

/* Children: adjust individual snap offset */
.slide {
  scroll-snap-align: start;
  scroll-margin-inline-start: 1rem;
}

The scroll-padding shorthand follows the same syntax as padding — one to four values for each edge.

Horizontal gallery pattern

A peek-through gallery where the next and previous slides are partially visible, encouraging the user to scroll:

.gallery {
  display: flex;
  gap: 1rem;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-padding-inline: 2rem;
  padding-inline: 2rem;
  scrollbar-width: none; /* hide scrollbar */
}

.gallery > img {
  flex: 0 0 calc(80% - 0.5rem);
  scroll-snap-align: center;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  border-radius: 0.75rem;
}
Always include scroll-padding when using padding on the scroll container — otherwise snap points won't account for the padding offset.

Vertical full-page sections

Scroll snap works on the vertical axis too. Full-page section layouts are a natural fit:

html {
  scroll-snap-type: y mandatory;
}

.page-section {
  scroll-snap-align: start;
  min-block-size: 100dvh;
  display: grid;
  place-items: center;
}

/* Stop snapping — let user scroll freely */
.page-section--long {
  scroll-snap-align: none;
}

Be cautious with mandatory on the root element — users with trackpads or accessibility needs may find it frustrating when they can't scroll freely past content.