A Comprehensive Guide to CSS Scroll Snap
For years, developers relied on JavaScript libraries to create UI patterns like image carousels, product sliders, and full-screen scrolling presentations. These solutions often meaningfully affected performance and could be complex to maintain. The CSS scroll snap module changes all of that, providing a powerful and performant CSS-only way to control the scrolling experience.
This guide will walk you through everything you need to know, from the fundamental properties to advanced techniques and common pitfalls.
What is CSS Scroll Snap?
At its core, CSS scroll snapping allows you to define specific points within a scrollable container where the browser should “snap” into place after the user finishes scrolling. Instead of the scrollport landing at an arbitrary position, it gracefully settles on a point you’ve defined, ensuring that elements are perfectly aligned and fully visible.
This creates a more predictable, controlled, and pleasant user experience.
The Two Key Players: Container and Children
Implementing scroll snap involves two main parts:
- The Scroll Container: The element that has overflow and is scrollable (e.g., using overflow: scroll). You “turn on” scroll snapping for this element.
- The Snap Points (Children): The direct children within the container that you want the viewport to snap to.
Let’s dive into the properties that control them.
The Fundamentals: scroll-snap-type and scroll-snap-align
You only need two properties to get a basic scroll snap working.
1. scroll-snap-type: The On/Off Switch
This property is set on the scroll container and does two things: it enables scroll snapping and defines how it behaves.
Syntax: scroll-snap-type: <axis> <strictness>;
The axis value (required) can be one of:
x: Snaps along the horizontal axis.y: Snaps along the vertical axis.both: Snaps along both axes independently.block: Snaps along the block axis (typically vertical).inline: Snaps along the inline axis (typically horizontal).
The strictness value (optional) can be one of:
mandatory: The browser must snap to a snap point when the user stops scrolling. This is great for UIs where you always want an item perfectly in view, like a photo gallery.proximity: The browser may snap to a snap point if the user stops scrolling close enough to it. This offers a more lenient experience. This is the default.
Example: A Vertical Scroller
Let’s create a simple vertical container where sections snap to the top.
HTML:
<div class="vertical-snapper">
<section><h1>Section 1</h1></section>
<section><h1>Section 2</h1></section>
<section><h1>Section 3</h1></section>
<section><h1>Section 4</h1></section>
</div>
CSS:
body {
margin: 0;
}
/* The scroll container */
.vertical-snapper {
height: 100vh;
overflow-y: scroll;
scroll-snap-type: y mandatory;
}
/* The snap points */
.vertical-snapper > section {
height: 100vh;
display: grid;
place-items: center;
scroll-snap-align: start; /* We'll explain this next! */
color: white;
}
/* Just for styling */
.vertical-snapper > section:nth-of-type(1) { background-color: #443730; }
.vertical-snapper > section:nth-of-type(2) { background-color: #A35709; }
.vertical-snapper > section:nth-of-type(3) { background-color: #F0A04B; }
.vertical-snapper > section:nth-of-type(4) { background-color: #1F1717; }
In this example, as you scroll, the viewport will always snap to the beginning of the next section because of scroll-snap-type: y mandatory.
2. scroll-snap-align: Where to Snap
This property is set on the child elements (the snap points) and tells the browser how they should align with the scroll container’s viewport when they are snapped to.
Values:
start: Aligns the start edge of the child with the start edge of the container’s viewport.center: Aligns the center of the child with the center of the container’s viewport.end: Aligns the end edge of the child with the end edge of the container’s viewport.none: Specifies that this element should not be a snap point.
You can also provide two values, one for the block axis and one for the inline axis (e.g., scroll-snap-align: start center;).
Example: A Horizontal Image Gallery
Here, we’ll make images snap to the center of the container.
HTML:
<div class="gallery">
<img src="https://placehold.co/400x300/443730/FFFFFF?text=Item+1">
<img src="https://placehold.co/400x300/A35709/FFFFFF?text=Item+2">
<img src="https://placehold.co/400x300/F0A04B/000000?text=Item+3">
<img src="https://placehold.co/400x300/1F1717/FFFFFF?text=Item+4">
</div>
CSS:
body {
display: grid;
place-items: center;
height: 100vh;
margin: 0;
}
/* The scroll container */
.gallery {
display: flex;
gap: 1rem;
width: 90%;
max-width: 800px;
overflow-x: scroll;
scroll-snap-type: x mandatory;
padding: 1rem; /* Add padding for visual appeal */
border-radius: 8px;
background: #f0f0f0;
}
/* The snap points */
.gallery > img {
flex: 0 0 80%; /* Images take up 80% of the container width */
width: 80%;
border-radius: 6px;
scroll-snap-align: center; /* notice this */
}
In this case, scroll-snap-align: center ensures that, no matter where you stop scrolling, an image will always be centered in the gallery view.
Fine-Tuning and Advanced Control
Sometimes the basics aren’t enough. CSS Scroll Snap provides additional properties for more granular control.
Keep Content Uncovered With scroll-padding
What if you have a fixed header at the top of your page? When you snap to an element, that fixed header might cover part of it. The scroll-padding property, applied to the scroll container, solves.
It works like the regular padding property, creating an inset that the browser will not snap content into.
Example: A Page with a Fixed Header
HTML:
<div class="example">
<header class="sticky-header">My Site Header</header>
<section>Section 1</section>
<section>Section 2</section>
<section>Section 3</section>
</div>
CSS:
body {
margin: 0;
}
.example {
height: 100vh; /* fills entire viewport */
overflow-y: auto;
scroll-snap-type: y mandatory;
scroll-behavior: smooth;
/* The magic! Leave 60px of breathing room */
scroll-padding-top: 60px;
}
/* Sticky header sits on top of the scrollport */
.sticky-header {
position: sticky;
top: 0;
height: 60px;
background: hsl(0deg 0% 100% / 65%);
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
z-index: 1;
}
/* Each section is full-screen height */
section {
height: 100vh;
scroll-snap-align: start;
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
color: white;
}
section:nth-child(odd) { background: #e74c3c; }
section:nth-child(even) { background: #8e44ad; }
Now, when an element snaps to the start, its top edge will align with the bottom of the 60px padding area, not the actual top of the viewport.
Adjusting Individual Snap Points With scroll-margin
While scroll-padding adjusts the container’s viewable area, scroll-margin adjusts the snapping area of an individual child element. Think of it as adding an “outset” that changes where the element aligns.
Example: Creating Visual Offset
Let’s modify our horizontal gallery to have items snap slightly off-center, peeking at the next item.
HTML:
<div class="gallery">
<img src="https://placehold.co/400x300/443730/FFFFFF?text=Item+1">
<img src="https://placehold.co/400x300/A35709/FFFFFF?text=Item+2">
<img src="https://placehold.co/400x300/F0A04B/000000?text=Item+3">
<img src="https://placehold.co/400x300/1F1717/FFFFFF?text=Item+4">
</div>
CSS:
body {
display: grid;
place-items: center;
height: 100vh;
margin: 0;
}
/* The scroll container */
.gallery {
display: flex;
gap: 1rem;
width: 90%;
max-width: 800px;
overflow-x: scroll;
scroll-snap-type: x mandatory;
padding: 1rem; /* Add padding for visual appeal */
border-radius: 8px;
background: #f0f0f0;
}
/* The snap points */
.gallery > img {
flex: 0 0 80%; /* Let images take up 80% of the container width */
width: 80%;
border-radius: 6px;
scroll-snap-align: start;
/* This pushes the snap point 50px to the left */
/* so the start of the image snaps 50px past the start of the container */
scroll-margin-left: 50px;
}
This really smooths out how your scrolling interfaces look and feel.
The scroll-padding and scroll-margin properties are related but serve distinct purposes. Here’s the difference:
scroll-paddingis for the parent. You apply it to the scroll container. It creates an inset (like a “no-snap zone”) around the edges of the viewport. This is useful for keeping all snapped items clear of a fixed element, like a header. It affects where all children can snap.scroll-marginis for the child. You apply it to the snap points. It creates an outset around that specific child, effectively changing the size of its snap area. This allows you to fine-tune the resting position of one or more specific items without affecting the others.
Think of it like this: scroll-padding sets the rules for the picture frame, while scroll-margin adds a custom border to an individual photo inside the frame.
Preventing “Skipping” With scroll-snap-stop
On touch devices, it’s easy to flick through a scroll container so fast that you skip over several snap points. The scroll-snap-stop property, applied to a child element, can prevent this.
Values:
normal: The user can scroll past snap points before settling. This is the default.always: Forces the scroll container to stop at the first snap point it encounters, even during a fast flick.
Try setting scroll-snap-align: center; and scroll-snap-stop: always; on the images in the last example and then try a fast swipe!
This technique is essential for step-by-step guides or any interface where you don’t want the user to accidentally miss an item.
Common Pitfalls and Nuances
While powerful, scroll snap has a few things to watch out for.
- Dynamic Content: If you add or remove items from the scroll container with JavaScript, the browser is smart enough to re-evaluate the snap points. However, if the currently snapped item is removed, the scroll position may jump. It’s good practice to manage the scroll position manually in such cases.
- Focus Management and Accessibility:
- The “Scroll Trap”: With scroll-snap-type: mandatory, keyboard users can get “trapped” if a scrollable element is smaller than the container’s viewport, as they might not be able to scroll far enough to reach the next focusable element. Ensure there is always a path forward.
- The
prefers-reduced-motionmedia feature: Some users find snapping disorienting. Respect their preferences by disabling the behavior inside a media query with thescroll-snap-type: none;declaration.
- Inconsistent Element Sizes: Scroll snapping works best with consistently sized child elements. If you have a mix of very large and very small items, the mandatory setting can feel jarring or unpredictable. proximity might be a better choice in these scenarios.
- Overusing Snap Points: It’s crucial to limit snap points to meaningful positions. A “meaningful position” is a point where a user would naturally want to pause, like the start of a new section or the center of an image. Applying scroll-snap-align to every element, like every paragraph in an article, can make scrolling feel “sticky” and frustrating, taking control away from the user. The goal is to assist, not hijack, the scroll.
- Nested Containers and Scroll Chaining: A common pitfall occurs when you nest scroll-snap containers (e.g., a horizontal gallery inside a vertical page). When a user scrolls to the end of the inner container, the scroll gesture can “chain” to the parent, causing the page to scroll unexpectedly. This can be fixed with the
overscroll-behaviorproperty on the inner container. Setting the property tocontainisolates the scrolling, providing a much more robust user experience. - Browser Compatibility: Support for the modern scroll snap spec is excellent across all major browsers. However, very old tutorials might mention deprecated properties like
scroll-snap-points-x. Stick to the properties outlined here for modern development.
Conclusion
CSS Scroll Snap is a mature, well-supported, and incredibly useful tool for modern web development. It allows you to build common, intuitive interfaces with minimal, declarative code, improving both user experience and performance. By moving complex logic from JavaScript to the browser’s rendering engine, you get smoother animations and a more maintainable codebase.
So go ahead, ditch that old JavaScript carousel library and give CSS Scroll Snap a try. Let the browser handle the heavy lifting!
By the way, Wirechunk is an excellent platform for building rich user experiences with modern CSS techniques.