Snapport
The container defines the snapping axes.
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.
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
The container defines the snapping axes.
Mandatory or proximity changes the pull.
The optimal viewing region moves inward.
The first alignment value controls block.
Each card creates its own snap area.
The second alignment value controls inline.
The snap area grows beyond the border box.
Always can prevent skipping a target.
Focus a card to bring it into view.
<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><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>.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;
}.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;
> * {
margin: 0;
}
h2 {
font-size: clamp(1.75rem, 1rem + 3vw, 3rem);
}
> span {
font-weight: 800;
}
&:focus-visible {
outline: 4px solid #172033;
outline-offset: -8px;
}
}
}Educational guide to scroll-snap
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.
The browser aligns a target area with a region of the container. Flexbox, Grid, or normal flow still handles the layout.
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.
Strictness depends on what people need to read, as well as the visual effect.
proximityUseful 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.
mandatoryUseful 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.
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.
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.
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.
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.
Keep the presence of more content visible and preserve a direct way to scroll.
Open the labReserve the region where a target should remain visible when aligned.
Open the labAdd explicit navigation when a sequence needs buttons or markers.
Open the labKeep the presence of more content visible and preserve a direct way to scroll.
Open the labReserve the region where a target should remain visible when aligned.
Open the labAdd explicit navigation when a sequence needs buttons or markers.
Open the labDo 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.
Diagnose the container first, then its targets. A snapping declaration may have no scrolling range to act on.
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.