0Support

CSS scroll snap

Explore how a scroll area settles into predictable positions. Change the axis, strictness, block and inline alignment, and the spacing around the snapport and each target.

Read the guide

Ask AI

CSS scroll snap

Response generated with openai/gpt-5.4-nano. AI can make mistakes. Always review the result.

Drag, use the wheel, or focus the cards with the keyboard to test every snap position.

Dashed line: snapport reduced by scroll-paddingDotted line: area expanded by scroll-margin

01

Snapport

The container defines the snapping axes.

02

Strictness

Mandatory or proximity changes the pull.

03

Padding

The optimal viewing region moves inward.

04

Block axis

The first alignment value controls block.

05

Snap target

Each card creates its own snap area.

06

Inline axis

The second alignment value controls inline.

07

Margin

The snap area grows beyond the border box.

08

Snap stop

Always can prevent skipping a target.

09

Keyboard

Focus a card to bring it into view.

HTML
<div class="snapport" aria-label="Interactive CSS scroll snap area" tabindex="0">
  <article
    class="snap-item"
    style="--item-color: #ffb38a"
    tabindex="0"
  >
    <span>01</span>
    <h2>Snapport</h2>
    <p>The container defines the snapping axes.</p>
  </article>
  <article
    class="snap-item"
    style="--item-color: #72d6c9"
    tabindex="0"
  >
    <span>02</span>
    <h2>Strictness</h2>
    <p>Mandatory or proximity changes the pull.</p>
  </article>
  <article
    class="snap-item"
    style="--item-color: #b6a8ff"
    tabindex="0"
  >
    <span>03</span>
    <h2>Padding</h2>
    <p>The optimal viewing region moves inward.</p>
  </article>
  <article
    class="snap-item"
    style="--item-color: #ffd166"
    tabindex="0"
  >
    <span>04</span>
    <h2>Block axis</h2>
    <p>The first alignment value controls block.</p>
  </article>
  <article
    class="snap-item"
    style="--item-color: #f7a6c4"
    tabindex="0"
  >
    <span>05</span>
    <h2>Snap target</h2>
    <p>Each card creates its own snap area.</p>
  </article>
  <article
    class="snap-item"
    style="--item-color: #8ecae6"
    tabindex="0"
  >
    <span>06</span>
    <h2>Inline axis</h2>
    <p>The second alignment value controls inline.</p>
  </article>
  <article
    class="snap-item"
    style="--item-color: #a7df9b"
    tabindex="0"
  >
    <span>07</span>
    <h2>Margin</h2>
    <p>The snap area grows beyond the border box.</p>
  </article>
  <article
    class="snap-item"
    style="--item-color: #f6bd60"
    tabindex="0"
  >
    <span>08</span>
    <h2>Snap stop</h2>
    <p>Always can prevent skipping a target.</p>
  </article>
  <article
    class="snap-item"
    style="--item-color: #c7a6ff"
    tabindex="0"
  >
    <span>09</span>
    <h2>Keyboard</h2>
    <p>Focus a card to bring it into view.</p>
  </article>
</div>
Tailwind CSS
Some classes with arbitrary values can be replaced by native classes, depending on the value and your Tailwind CSS configuration.
<div class="grid grid-cols-[repeat(3,72%)] auto-rows-[72%] gap-6 inline-[min(100%,36rem)] block-96 m-0 p-0 overflow-auto overscroll-contain snap-both snap-mandatory scroll-p-[32px]" aria-label="Interactive CSS scroll snap area" tabindex="0">
  <article
    class="box-border grid content-end gap-2 p-[clamp(20px,4vw,32px)] border rounded-2xl text-[#172033] scroll-m-[12px] snap-center snap-normal *:m-0 [&_h2]:text-[clamp(1.75rem,1rem_+_3vw,3rem)] [&>span]:font-extrabold focus-visible:outline-4 focus-visible:outline-[#172033] focus-visible:-outline-offset-8 border-[color-mix(in_oklch,#ffb38a_72%,#172033)] bg-[#ffb38a]"
    tabindex="0"
  >
    <span>01</span>
    <h2>Snapport</h2>
    <p>The container defines the snapping axes.</p>
  </article>
  <article
    class="box-border grid content-end gap-2 p-[clamp(20px,4vw,32px)] border rounded-2xl text-[#172033] scroll-m-[12px] snap-center snap-normal *:m-0 [&_h2]:text-[clamp(1.75rem,1rem_+_3vw,3rem)] [&>span]:font-extrabold focus-visible:outline-4 focus-visible:outline-[#172033] focus-visible:-outline-offset-8 border-[color-mix(in_oklch,#72d6c9_72%,#172033)] bg-[#72d6c9]"
    tabindex="0"
  >
    <span>02</span>
    <h2>Strictness</h2>
    <p>Mandatory or proximity changes the pull.</p>
  </article>
  <article
    class="box-border grid content-end gap-2 p-[clamp(20px,4vw,32px)] border rounded-2xl text-[#172033] scroll-m-[12px] snap-center snap-normal *:m-0 [&_h2]:text-[clamp(1.75rem,1rem_+_3vw,3rem)] [&>span]:font-extrabold focus-visible:outline-4 focus-visible:outline-[#172033] focus-visible:-outline-offset-8 border-[color-mix(in_oklch,#b6a8ff_72%,#172033)] bg-[#b6a8ff]"
    tabindex="0"
  >
    <span>03</span>
    <h2>Padding</h2>
    <p>The optimal viewing region moves inward.</p>
  </article>
  <article
    class="box-border grid content-end gap-2 p-[clamp(20px,4vw,32px)] border rounded-2xl text-[#172033] scroll-m-[12px] snap-center snap-normal *:m-0 [&_h2]:text-[clamp(1.75rem,1rem_+_3vw,3rem)] [&>span]:font-extrabold focus-visible:outline-4 focus-visible:outline-[#172033] focus-visible:-outline-offset-8 border-[color-mix(in_oklch,#ffd166_72%,#172033)] bg-[#ffd166]"
    tabindex="0"
  >
    <span>04</span>
    <h2>Block axis</h2>
    <p>The first alignment value controls block.</p>
  </article>
  <article
    class="box-border grid content-end gap-2 p-[clamp(20px,4vw,32px)] border rounded-2xl text-[#172033] scroll-m-[12px] snap-center snap-normal *:m-0 [&_h2]:text-[clamp(1.75rem,1rem_+_3vw,3rem)] [&>span]:font-extrabold focus-visible:outline-4 focus-visible:outline-[#172033] focus-visible:-outline-offset-8 border-[color-mix(in_oklch,#f7a6c4_72%,#172033)] bg-[#f7a6c4]"
    tabindex="0"
  >
    <span>05</span>
    <h2>Snap target</h2>
    <p>Each card creates its own snap area.</p>
  </article>
  <article
    class="box-border grid content-end gap-2 p-[clamp(20px,4vw,32px)] border rounded-2xl text-[#172033] scroll-m-[12px] snap-center snap-normal *:m-0 [&_h2]:text-[clamp(1.75rem,1rem_+_3vw,3rem)] [&>span]:font-extrabold focus-visible:outline-4 focus-visible:outline-[#172033] focus-visible:-outline-offset-8 border-[color-mix(in_oklch,#8ecae6_72%,#172033)] bg-[#8ecae6]"
    tabindex="0"
  >
    <span>06</span>
    <h2>Inline axis</h2>
    <p>The second alignment value controls inline.</p>
  </article>
  <article
    class="box-border grid content-end gap-2 p-[clamp(20px,4vw,32px)] border rounded-2xl text-[#172033] scroll-m-[12px] snap-center snap-normal *:m-0 [&_h2]:text-[clamp(1.75rem,1rem_+_3vw,3rem)] [&>span]:font-extrabold focus-visible:outline-4 focus-visible:outline-[#172033] focus-visible:-outline-offset-8 border-[color-mix(in_oklch,#a7df9b_72%,#172033)] bg-[#a7df9b]"
    tabindex="0"
  >
    <span>07</span>
    <h2>Margin</h2>
    <p>The snap area grows beyond the border box.</p>
  </article>
  <article
    class="box-border grid content-end gap-2 p-[clamp(20px,4vw,32px)] border rounded-2xl text-[#172033] scroll-m-[12px] snap-center snap-normal *:m-0 [&_h2]:text-[clamp(1.75rem,1rem_+_3vw,3rem)] [&>span]:font-extrabold focus-visible:outline-4 focus-visible:outline-[#172033] focus-visible:-outline-offset-8 border-[color-mix(in_oklch,#f6bd60_72%,#172033)] bg-[#f6bd60]"
    tabindex="0"
  >
    <span>08</span>
    <h2>Snap stop</h2>
    <p>Always can prevent skipping a target.</p>
  </article>
  <article
    class="box-border grid content-end gap-2 p-[clamp(20px,4vw,32px)] border rounded-2xl text-[#172033] scroll-m-[12px] snap-center snap-normal *:m-0 [&_h2]:text-[clamp(1.75rem,1rem_+_3vw,3rem)] [&>span]:font-extrabold focus-visible:outline-4 focus-visible:outline-[#172033] focus-visible:-outline-offset-8 border-[color-mix(in_oklch,#c7a6ff_72%,#172033)] bg-[#c7a6ff]"
    tabindex="0"
  >
    <span>09</span>
    <h2>Keyboard</h2>
    <p>Focus a card to bring it into view.</p>
  </article>
</div>
CSS
.snapport {
  display: grid;
  grid-template-columns: repeat(3, 72%);
  grid-auto-rows: 72%;
  gap: 1.5rem;
  inline-size: min(100%, 36rem);
  block-size: 24rem;
  margin: 0;
  padding: 0;
  overflow: auto;
  overscroll-behavior: contain;
  scroll-snap-type: both mandatory;
  scroll-padding: 32px;
}

.snap-item {
  box-sizing: border-box;
  display: grid;
  align-content: end;
  gap: 0.5rem;
  padding: clamp(20px, 4vw, 32px);
  border: 1px solid color-mix(in oklch, var(--item-color) 72%, #172033);
  border-radius: 1rem;
  background: var(--item-color);
  color: #172033;
  scroll-margin: 12px;
  scroll-snap-align: center;
  scroll-snap-stop: normal;
}

.snap-item > * {
  margin: 0;
}

.snap-item h2 {
  font-size: clamp(1.75rem, 1rem + 3vw, 3rem);
}

.snap-item > span {
  font-weight: 800;
}

.snap-item:focus-visible {
  outline: 4px solid #172033;
  outline-offset: -8px;
}

Educational guide to scroll-snap

Give scrolling rhythm while keeping control

Scroll snap helps a gallery, card list, or sequence of sections finish scrolling at useful positions. Learn to separate the scroll area from its targets and choose snapping that preserves reading and navigation.

Back to the playground

Mental model

The browser aligns a target area with a region of the container. Flexbox, Grid, or normal flow still handles the layout.

Two regions that alignStatic diagram, not drawn to scale. Borders and numbers identify each part; the recipes below let you try native snapping.
  1. 1 · ScrollportThe visible window of the container with overflow.
  2. 2 · SnapportThe alignment region inside the scrollport, bounded by scroll-padding.
  3. 3 · Target boxThe border box of the element participating through scroll-snap-align.
  4. 4 · Snap areaThe target region after scroll-margin expands it; the dotted border represents that expansion.

First you need actual overflow. A card row can use display: flex, items that do not shrink, and overflow: auto. Apply scroll-snap-type to that container; it does not create the layout, a scrollbar, or overflowing content by itself.

Each target supplies a position through scroll-snap-align. The initial value none supplies no position. Targets belong to the nearest relevant scroll ancestor: setting the property on an outer wrapper does not configure an inner scroller.

Choose the snapping strictness

Strictness depends on what people need to read, as well as the visual effect.

proximity

Useful for cards and continuous reading. The browser may snap when you finish near a target and allows stopping between positions. The distance it considers near depends on the browser and input; there is no configurable pixel threshold.

mandatory

Useful for a sequence with clear targets and controlled content. After scrolling ends, the container must settle at a valid snap position if one exists. Check variable heights, zoom, and keyboard use before applying it to reading content.

A target larger than the snapport is not automatically unreadable: the specification allows scrolling inside it while it covers that region. Problems arise with content outside snap areas, rigid heights, or positions that do not fit reading. For long articles, start with proximity and check that the beginning, middle, and end remain reachable with enlarged text.

scroll-snap-stop: normal allows passing targets during a scroll; always prevents passing over a position during operations that traverse intermediate positions. It does not turn proximity into mandatory or require stopping at every target when jumping directly to a position, for example through a link.

Axes, alignment, and spacing

The scrolling axis, snap position, and visual spacing are separate decisions.

x and y are physical axes; inline and block follow the writing mode. In horizontal-tb, inline is horizontal and block is vertical. both enables snapping in both axes and requires a scenario with scrolling along them; it does not turn a row into a grid.

start, center, and end align the start, center, or end of a target area with the snapport. A single scroll-snap-align value applies to both axes. With two values, the first is block and the second inline: none start defines a position only on inline, rather than meaning “x and y”.

scroll-padding belongs to the container and reduces its alignment region. scroll-margin belongs to the target and expands its snap area. Neither takes up layout space: padding, margin, and gap are still needed to separate boxes visually.

Reserve space for a header

Snippet for a vertical list with actual overflow: 4rem reserve the header space and 0.5rem add separation for each target. Match the reservation to the real header size, including at zoom.

Reserve space for a header
CSS
.reading-list {
  overflow: auto;
  scroll-snap-type: block proximity;
  scroll-padding-block-start: 4rem;

  > article {
    scroll-snap-align: start;
    scroll-margin-block-start: 0.5rem;
  }
}

With start alignment, those distances combine to separate the target from the edge. Scroll limits still apply: the first or last card may not reach the ideal alignment. scroll-padding does not add empty scrolling space at either end; add layout spacing if the design needs it.

A reusable horizontal list

Two lists with the same cards and dimensions let you compare strictness. Only mandatory and proximity change in the CSS.

The cards keep real HTML links and let the next card peek into view. Each list has an accessible name, its own focus, and a native scrollbar, and works without JavaScript. The CSS shows explicit color, radius, and spacing values; light-dark() preserves both themes with color-scheme. You can adapt those values to your project.

Reading with proximity

01 Scrollbars

Keep the presence of more content visible and preserve a direct way to scroll.

Open the lab
02 Container spacing

Reserve the region where a target should remain visible when aligned.

Open the lab
03 CSS carousel

Add explicit navigation when a sequence needs buttons or markers.

Open the lab
Scroll the list horizontally using the scrollbar or a device gesture. With the region focused, try the arrow keys; Tab visits its links. It may snap near a target, but does not have to snap at every stop.
Reading with proximity
<div class="snap-guide-proximity" role="region" aria-label="Lab list with proximity snapping" tabindex="0">
  <article>
    <span>01</span>
    <strong>Scrollbars</strong>
    <p>Keep the presence of more content visible and preserve a direct way to scroll.</p>
    <a href="/en/css/scroll/scrollbar/">Open the lab</a>
  </article>
  <article>
    <span>02</span>
    <strong>Container spacing</strong>
    <p>Reserve the region where a target should remain visible when aligned.</p>
    <a href="/en/css/scroll/scroll-padding/">Open the lab</a>
  </article>
  <article>
    <span>03</span>
    <strong>CSS carousel</strong>
    <p>Add explicit navigation when a sequence needs buttons or markers.</p>
    <a href="/en/css/scroll/css-carousel/">Open the lab</a>
  </article>
</div>
:root {
  color-scheme: light dark;
}

.snap-guide-proximity {
  display: flex;
  gap: 1rem;
  min-inline-size: 0;
  padding: 1rem;
  overflow: auto;
  scroll-snap-type: inline proximity;
  scroll-padding-inline: 1.5rem;
  border: 1px solid light-dark(oklch(0.84 0.01 95), oklch(0.36 0.024 260));
  border-radius: 16px;
  corner-shape: squircle;
  background: light-dark(oklch(0.98 0.005 95), oklch(0.16 0.02 260));
  color: light-dark(oklch(0.22 0.01 95), oklch(0.95 0.01 95));

  > article {
    box-sizing: border-box;
    flex: 0 0 82%;
    min-inline-size: 0;
    display: grid;
    align-content: start;
    gap: 0.5rem;
    padding: 1rem;
    border: 1px solid light-dark(oklch(0.84 0.01 95), oklch(0.36 0.024 260));
    border-radius: 16px;
    corner-shape: squircle;
    background: light-dark(color-mix(in oklch, oklch(0.95 0.008 95) 98%, white), color-mix(in oklch, oklch(0.22 0.024 260) 86%, black));
    overflow-wrap: anywhere;
    scroll-snap-align: start;
    scroll-snap-stop: normal;
    scroll-margin-inline: 0.5rem;

    > * {
      margin: 0;
    }

    > span,
    > strong {
      font-weight: 800;
    }
  }

  a {
    color: light-dark(oklch(0.46 0.14 261), oklch(0.82 0.18 261));
    text-decoration: underline;
    text-underline-offset: 0.2em;
  }

  &:focus-visible,
  a:focus-visible {
    outline: 3px dashed light-dark(oklch(0.46 0.14 261), oklch(0.82 0.18 261));
    outline-offset: -3px;
  }
}

The same list with mandatory

01 Scrollbars

Keep the presence of more content visible and preserve a direct way to scroll.

Open the lab
02 Container spacing

Reserve the region where a target should remain visible when aligned.

Open the lab
03 CSS carousel

Add explicit navigation when a sequence needs buttons or markers.

Open the lab
Repeat the journey: a valid position is chosen when scrolling ends. The final card is constrained by the scroll boundary. Motion and the chosen position may vary with the browser and input.
The same list with mandatory
<div class="snap-guide-mandatory" role="region" aria-label="Lab list with mandatory snapping" tabindex="0">
  <article>
    <span>01</span>
    <strong>Scrollbars</strong>
    <p>Keep the presence of more content visible and preserve a direct way to scroll.</p>
    <a href="/en/css/scroll/scrollbar/">Open the lab</a>
  </article>
  <article>
    <span>02</span>
    <strong>Container spacing</strong>
    <p>Reserve the region where a target should remain visible when aligned.</p>
    <a href="/en/css/scroll/scroll-padding/">Open the lab</a>
  </article>
  <article>
    <span>03</span>
    <strong>CSS carousel</strong>
    <p>Add explicit navigation when a sequence needs buttons or markers.</p>
    <a href="/en/css/scroll/css-carousel/">Open the lab</a>
  </article>
</div>
:root {
  color-scheme: light dark;
}

.snap-guide-mandatory {
  display: flex;
  gap: 1rem;
  min-inline-size: 0;
  padding: 1rem;
  overflow: auto;
  scroll-snap-type: inline mandatory;
  scroll-padding-inline: 1.5rem;
  border: 1px solid light-dark(oklch(0.84 0.01 95), oklch(0.36 0.024 260));
  border-radius: 16px;
  corner-shape: squircle;
  background: light-dark(oklch(0.98 0.005 95), oklch(0.16 0.02 260));
  color: light-dark(oklch(0.22 0.01 95), oklch(0.95 0.01 95));

  > article {
    box-sizing: border-box;
    flex: 0 0 82%;
    min-inline-size: 0;
    display: grid;
    align-content: start;
    gap: 0.5rem;
    padding: 1rem;
    border: 1px solid light-dark(oklch(0.84 0.01 95), oklch(0.36 0.024 260));
    border-radius: 16px;
    corner-shape: squircle;
    background: light-dark(color-mix(in oklch, oklch(0.95 0.008 95) 98%, white), color-mix(in oklch, oklch(0.22 0.024 260) 86%, black));
    overflow-wrap: anywhere;
    scroll-snap-align: start;
    scroll-snap-stop: normal;
    scroll-margin-inline: 0.5rem;

    > * {
      margin: 0;
    }

    > span,
    > strong {
      font-weight: 800;
    }
  }

  a {
    color: light-dark(oklch(0.46 0.14 261), oklch(0.82 0.18 261));
    text-decoration: underline;
    text-underline-offset: 0.2em;
  }

  &:focus-visible,
  a:focus-visible {
    outline: 3px dashed light-dark(oklch(0.46 0.14 261), oklch(0.82 0.18 261));
    outline-offset: -3px;
  }
}

Do not hide content or cancel native link behavior to achieve snapping. Test keyboard, touch, and wheel separately; make sure focus stays visible. Avoid autoplay and unnecessary smooth scrolling; if you add animation, respect prefers-reduced-motion.

Common mistakes

Diagnose the container first, then its targets. A snapping declaration may have no scrolling range to act on.

I declare snap, but nothing scrolls

Expected result
There is scrolling space and every card is reachable.
Cause
The content fits or flex items shrink; overflow alone does not force overflowing content.
Solution
Compare scrollWidth with clientWidth (or the heights). Size the container and cards to produce overflow; in a row, prevent shrinking with flex: 0 0 ….

The container moves without snapping

Expected result
Elements supply positions along the active axis.
Cause
scroll-snap-align is missing, is none, or the properties are on a different container from the one that scrolls.
Solution
Set scroll-snap-type on the actual scroller and scroll-snap-align on its targets. Do not mistake a lack of snapping far from a target for broken proximity.

Snapping happens on the wrong axis

Expected result
The position follows the writing mode and scrolling direction.
Cause
x/y are confused with inline/block, or the two scroll-snap-align values are read as horizontal/vertical.
Solution
Check writing-mode and the overflowing axis. Remember the order block inline; use inline or block when the logical direction should follow the text.

The header covers the target

Expected result
The heading and its focus remain visible when scrolling.
Cause
Layout padding or margin changed, but the alignment region beneath the header did not.
Solution
Reserve the header with scroll-padding-block-start on the actual container. Use scroll-margin-block-start for extra per-target spacing; check the measurement with enlarged text.

always does not stop at every card

Expected result
A position is not passed over during a scroll to which that constraint applies.
Cause
scroll-snap-stop: always is expected to control a direct target jump or proximity strictness as well.
Solution
Try a gesture that crosses positions, as well as links or coordinate jumps. Choose strictness through scroll-snap-type and reserve always for cases where slowing the journey adds value.

Support and compatibility

Separate basic snapping from APIs that build navigation and observe its changes.

The basic properties scroll-snap-type, scroll-snap-align, scroll-snap-stop, scroll-padding, and scroll-margin are widely available according to MDN. That status does not guarantee identical physics, proximity distance, or final positions in every browser.

If snapping declarations are ignored, the recipes keep normal scrolling and usable links. They do not need a polyfill to reach the content. The preview shows your browser’s implementation, rather than certifying support across engines.

Scroll snap supplies positions; a carousel adds navigation and state. It does not create buttons, markers, or accessible announcements. Support for scroll buttons, scroll markers, snap events, or state queries needs separate checks; none of those pieces are used here.