Introduction to Scroll Snap
You have certainly already visited websites where the scroll feels "magnetic" β the page stops automatically on each section in a smooth and precise way. Before CSS Scroll Snap, this type of experience required complex JavaScript libraries like fullPage.js or manual position calculations.
Today, thanks to CSS Scroll Snap, you can create these experiences natively, with just a few lines of CSS. The result? Optimal performance, excellent browser compatibility, and zero JavaScript dependency.
CSS Scroll Snap is supported by all modern browsers since 2019 (Chrome, Firefox, Safari, Edge). More than 96% of users can benefit from it without any polyfill.
In this complete guide, we'll explore all Scroll Snap properties, understand their differences, and build practical demos you can directly reuse in your projects.
The scroll-snap-type property
The scroll-snap-type property is the starting point of any implementation. It is defined on the scrollable container (the parent element) and accepts two parameters: the axis and the behavior.
Basic syntax
/* Syntax: scroll-snap-type: [axis] [behavior]; */
/* Mandatory horizontal scroll */
.container {
scroll-snap-type: x mandatory;
}
/* Optional vertical scroll */
.container {
scroll-snap-type: y proximity;
}
/* Both axes at the same time */
.container {
scroll-snap-type: both mandatory;
}
Axis values
| Value | Description | Use case |
|---|---|---|
x |
Horizontal snap only | Carousels, horizontal galleries |
y |
Vertical snap only | Full-screen sections, landing pages |
both |
Snap on both axes | Image grids, interactive tables |
none |
Disables snap | Conditional deactivation |
Mandatory vs Proximity
This is where the magic happens. The difference between mandatory and proximity is crucial for the user experience.
- mandatory: The scroll ALWAYS stops on a snap point. Even a small scroll will force alignment. Ideal for full-screen sections where the user must see each section completely.
- proximity: Snap only activates if the user is close to a snap point. Allows freer scrolling while still offering contextual alignment.
Use mandatory with caution on containers with variable content. If an element is larger than the viewport, the user could get stuck without being able to see all the content.
The scroll-snap-align property
Once the container is configured, each child element can define its alignment point with scroll-snap-align. This property is set on the child elements.
The three main values
/* Element aligns to the start of the container */
.item {
scroll-snap-align: start;
}
/* Element aligns to the center of the container */
.item {
scroll-snap-align: center;
}
/* Element aligns to the end of the container */
.item {
scroll-snap-align: end;
}
/* No snap for this element */
.item {
scroll-snap-align: none;
}
Here is an interactive demonstration showing the difference between the three alignments:
Values on two axes
You can also specify different alignments for each axis:
/* Syntax: scroll-snap-align: [block] [inline]; */
/* Vertical center, horizontal start */
.item {
scroll-snap-align: center start;
}
/* Vertical start, horizontal center */
.item {
scroll-snap-align: start center;
}
Creating full-screen sections
One of the most popular use cases for Scroll Snap is creating full-screen sections, as seen on many modern landing pages.
/* Main container - takes full viewport */
.fullpage-container {
height: 100vh;
overflow-y: auto;
scroll-snap-type: y mandatory;
/* Hide the scrollbar (optional) */
scrollbar-width: none;
-ms-overflow-style: none;
}
.fullpage-container::-webkit-scrollbar {
display: none;
}
/* Each section takes 100% of the viewport */
.section {
height: 100vh;
scroll-snap-align: start;
/* Content centering */
display: flex;
align-items: center;
justify-content: center;
}
/* Different colors for each section */
.section:nth-child(1) {
background: linear-gradient(135deg, #6366f1, #8b5cf6);
}
.section:nth-child(2) {
background: linear-gradient(135deg, #8b5cf6, #d946ef);
}
.section:nth-child(3) {
background: linear-gradient(135deg, #d946ef, #f43f5e);
}
And the corresponding HTML:
<div class="fullpage-container">
<section class="section">
<h1>Welcome</h1>
<p>Discover our product</p>
</section>
<section class="section">
<h2>Features</h2>
<!-- Content -->
</section>
<section class="section">
<h2>Pricing</h2>
<!-- Content -->
</section>
</div>
Creating a horizontal carousel
Carousels are another perfect use case for Scroll Snap. Here is how to create a carousel of product cards or testimonials, without a single line of JavaScript.
.carousel {
display: flex;
gap: 20px;
padding: 20px;
/* Enable horizontal scroll */
overflow-x: auto;
scroll-snap-type: x mandatory;
/* Hide the scrollbar */
scrollbar-width: none;
-ms-overflow-style: none;
}
.carousel::-webkit-scrollbar {
display: none;
}
.carousel-card {
/* Fixed size for each card */
flex: 0 0 280px;
/* Snap point at start of each card */
scroll-snap-align: start;
/* Styles visuels */
background: #12121a;
border: 1px solid rgba(255,255,255,0.08);
border-radius: 16px;
padding: 24px;
transition: transform 0.3s ease;
}
.carousel-card:hover {
transform: translateY(-4px);
}
Add padding to the container so the first and last elements are not stuck to the edges. The snap will still work correctly.
scroll-padding and accessibility
The scroll-padding property is often overlooked but it is crucial for accessibility and user experience, especially when you have a fixed navigation at the top of the page.
The fixed headers problem
Imagine: you have a fixed header that is 80px tall and full-screen sections. Without scroll-padding, the top of each section will be hidden behind your header!
/* Container with a fixed 80px header */
.container {
scroll-snap-type: y mandatory;
/* Compensates for header height */
scroll-padding-top: 80px;
}
/* Variant with padding on all sides */
.container {
scroll-padding: 80px 20px 20px 20px;
}
/* For a horizontal carousel with lateral padding */
.carousel {
scroll-snap-type: x mandatory;
scroll-padding-inline: 24px;
}
scroll-margin: the alternative on children
You can also use scroll-margin on child elements for finer control:
/* Applied on each child element */
.section {
scroll-snap-align: start;
/* 80px scroll margin at top */
scroll-margin-top: 80px;
}
/* Useful for different margins per element */
.hero-section {
scroll-margin-top: 0; /* No margin for hero */
}
.content-section {
scroll-margin-top: 80px; /* Margin for header */
}
scroll-padding is set on the container and affects all children. scroll-margin is set on individual children for more precise control.
Accessibility and prefers-reduced-motion
Some users are sensitive to abrupt movements. Although Scroll Snap does not directly animate the scroll, the "magnetic" behavior can be disorienting. Here is how to respect user preferences:
/* Default configuration */
.container {
scroll-snap-type: y mandatory;
scroll-behavior: smooth;
}
/* Disable for sensitive users */
@media (prefers-reduced-motion: reduce) {
.container {
/* Switch to proximity for less aggressive behavior */
scroll-snap-type: y proximity;
scroll-behavior: auto;
}
}
Advanced practical examples
Now that you have mastered the basics, let's look at some more advanced patterns you can implement.
Image gallery with scroll snap
A gallery that allows natural navigation between images:
.gallery {
display: grid;
grid-auto-flow: column;
grid-auto-columns: 100%;
gap: 0;
overflow-x: auto;
scroll-snap-type: x mandatory;
overscroll-behavior-x: contain;
scrollbar-width: none;
}
.gallery-image {
scroll-snap-align: start;
width: 100%;
height: 400px;
object-fit: cover;
}
Scroll snap with indicators
Add pagination indicators that update automatically. This requires a little JavaScript:
const carousel = document.querySelector('.carousel');
const items = carousel.querySelectorAll('.carousel-item');
const dots = document.querySelectorAll('.dot');
// Detect visible element after scroll
carousel.addEventListener('scroll', () => {
const scrollLeft = carousel.scrollLeft;
const itemWidth = items[0].offsetWidth;
const currentIndex = Math.round(scrollLeft / itemWidth);
// Update the indicators
dots.forEach((dot, index) => {
dot.classList.toggle('active', index === currentIndex);
});
});
// Click on indicator to navigate
dots.forEach((dot, index) => {
dot.addEventListener('click', () => {
items[index].scrollIntoView({
behavior: 'smooth',
inline: 'start'
});
});
});
Stop scroll with scroll-snap-stop
The scroll-snap-stop property controls whether the user can "skip" over elements during a fast scroll:
/* By default: user can skip elements */
.item {
scroll-snap-stop: normal;
}
/* Forces stop on each element */
.item-important {
scroll-snap-stop: always;
}
/* Use case: step-by-step tutorial */
.tutorial-step {
scroll-snap-align: start;
scroll-snap-stop: always; /* Forces viewing each step */
}
scroll-snap-stop is not supported on Firefox (Chrome/Safari only). Use it as a progressive enhancement.
Best practices and errors to avoid
Before concluding, here is a summary of best practices for using CSS Scroll Snap effectively:
To do
- Test on mobile: Touch behavior can differ from mouse scroll
- Use scroll-padding for fixed headers
- Prefer proximity for variable-length content
- Add overscroll-behavior to avoid conflicts with the parent scroll
- Respect prefers-reduced-motion for accessibility
To avoid
- mandatory on variable content: Can lock the user in place
- Snap on body/html directly: Compatibility issues
- Forgetting padding: Elements cut off by the edges
- Too many snap points: Frustrating experience
/* Recommended configuration */
.snap-container {
scroll-snap-type: y mandatory;
scroll-padding-top: 80px;
overscroll-behavior: contain;
}
.snap-item {
scroll-snap-align: start;
}
/* Accessibility */
@media (prefers-reduced-motion: reduce) {
.snap-container {
scroll-snap-type: y proximity;
}
}
/* Mobile-first */
@media (max-width: 768px) {
.snap-container {
scroll-padding-top: 60px; /* Smaller header on mobile */
}
}
Conclusion
CSS Scroll Snap is a powerful tool that allows you to create smooth and engaging navigation experiences, without relying on heavy JavaScript libraries. Whether for full-screen sections, product carousels, or image galleries, the possibilities are numerous.
Key takeaways:
scroll-snap-typeon the container to enable snapscroll-snap-alignon children to define alignment pointsmandatoryfor strict control,proximityfor more freedomscroll-paddingto compensate for fixed elements- Always consider accessibility with
prefers-reduced-motion
Don't hesitate to experiment with the different properties to find the perfect behavior for your project. Browser support is excellent, and your users will appreciate this extra touch of polish!
Discover our templates with pre-integrated Scroll Snap in our effects library, ready to be used in your projects.