A carousel that lands halfway between two cards feels unfinished. The usual fix used to be a JavaScript loop that watched scroll position, guessed the nearest item, and corrected the offset. CSS Scroll Snap gives that job to the browser.
That can make the implementation smaller and the interaction more faithful to the device. It is not a magic performance switch. The useful gain comes from deleting work that JavaScript no longer needs to do.
Scroll snap and smooth scrolling solve different problems
Scroll snap decides where a scroll may finish. Smooth scrolling decides how anchor navigation or a programmatic scroll moves to its destination. You can use either feature on its own, or combine them.
Destination
.rail {
scroll-snap-type: inline proximity;
}
.card {
scroll-snap-align: start;
}
Movement
html {
scroll-behavior: smooth;
}
The scroll-behavior property does not change a person's wheel, trackpad, or touch gesture. It applies when navigation or a scrolling API moves the page. Scroll snap still runs after the scroll operation and may adjust the final position.
Build a useful card rail first
Start with ordinary overflow. If scroll snap is unavailable or removed, the content should still be reachable.
<ul class="card-rail" aria-label="Featured articles" tabindex="0">
<li class="card">...</li>
<li class="card">...</li>
<li class="card">...</li>
</ul>
.card-rail {
display: grid;
grid-auto-flow: column;
grid-auto-columns: min(82%, 22rem);
gap: 1rem;
overflow-x: auto;
overscroll-behavior-inline: contain;
padding: 0 1rem 1rem;
scroll-snap-type: inline proximity;
scroll-padding-inline: 1rem;
}
.card {
scroll-snap-align: start;
}
The logical inline axis follows the writing mode. The rail remains horizontally scrollable in a left-to-right interface, while the code avoids hard-coding physical directions into the snapping rule.
Keep a visible scrolling cue. A clipped next card or a scrollbar tells people there is more content. Snap behavior does not replace an affordance.
Choose proximity before mandatory
With proximity, the browser snaps when the scroll ends near a snap point. With mandatory, it must settle on one. Mandatory snapping suits fixed-size slides and short galleries. It is risky when an item can grow taller or wider than the scrollport.
.long-form-sections {
block-size: 100dvh;
overflow-y: auto;
scroll-snap-type: block mandatory;
}
If one section is taller than the container, mandatory snapping can make part of it difficult or impossible to reach. This is why I treat proximity as the default and reserve mandatory for content with controlled dimensions.
scroll-snap-stop: always can force a stop on a particular item. Use it sparingly. Making every card unavoidable turns a quick flick through a long list into a chore.
Use scroll padding and scroll margin for real layouts
A fixed header changes the visible part of the scrollport. Without an offset, a snapped heading can land underneath it. Put the shared offset on the scrolling container with scroll-padding. Put a one-off offset on a target with scroll-margin.
:root {
--header-size: 4.5rem;
}
html {
scroll-padding-block-start: calc(var(--header-size) + 1rem);
}
.article-section {
scroll-margin-block-start: 2rem;
}
This also improves normal fragment links such as href="#pricing". The destination gets breathing room without adding invisible spacer elements or subtracting header heights in JavaScript.
Add smooth scrolling as a preference-aware enhancement
Smooth anchor navigation can help people keep their sense of position. Large pans can also trigger discomfort, so the reduced-motion branch should return to instant movement.
@media (prefers-reduced-motion: no-preference) {
html {
scroll-behavior: smooth;
}
}
Putting the feature inside the no-preference query keeps the default behavior for people who have asked their operating system to reduce motion. There is no need to reproduce a duration or easing curve in JavaScript. The user agent chooses both.
The performance claim needs evidence
Replacing a scroll listener, repeated layout reads, and corrective calls to scrollTo() can reduce main-thread work. Native scroll snap also has access to the browser's scrolling machinery in a way a script does not. That is a good reason to prefer it when the behavior matches.
It still does not guarantee a better Largest Contentful Paint, Interaction to Next Paint, or Cumulative Layout Shift score. Those metrics depend on the rest of the page. Measure before and after, and look for the work you actually removed:
- Scroll and resize listeners that calculate a destination.
- Layout reads such as
getBoundingClientRect()inside a scroll path. - Animation libraries loaded only to settle a carousel on an item.
- Duplicated active-index state used only to mirror the visible card.
A smaller script is useful even when a synthetic performance score barely moves. There is less code to execute, debug, and keep synchronized with layout changes.
What to test before shipping
- Trackpad, mouse wheel, touch, and keyboard scrolling all reach every item.
- Content remains usable at 200% zoom and when a card becomes taller than expected.
- A fixed header does not cover fragment-link or snap destinations.
- Reduced-motion mode uses instant scrolling.
- The unsnapped overflow layout still makes sense as a fallback.
Scroll snap is a good fit for galleries, short card rails, onboarding steps, and other interfaces with meaningful item boundaries. It is a poor fit for forcing every section of a long document to occupy the viewport. The browser can own the snapping without the design owning the user's scroll.