Blog / CSS

CSS Scroll Snap: The Complete Guide to Smooth Navigation

Master CSS Scroll Snap to create magnetic scroll experiences: full-screen sections, native carousels and fluid galleries. No JavaScript, compatible with all browsers.

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.

πŸ’‘
Good to know

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

scroll-snap-type.css
/* 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.
mandatory
1
2
3
4
proximity
1
2
3
4
Scroll to compare the behaviors
⚠️
Caution with mandatory

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

scroll-snap-align.css
/* 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:

start
center
end
start
center
Notice how each element aligns differently

Values on two axes

You can also specify different alignments for each axis:

snap-align-dual.css
/* 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.

Section Hero Impactful first impression
Features Showcase your benefits
Testimonials Social proof
Call to Action Convert your visitors
Scroll vertically in the demo
fullpage-sections.css
/* 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:

fullpage-sections.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>

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.

Scroll through the cards
carousel.css
.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);
}
πŸ’‘
Tip: Padding for the first/last element

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!

scroll-padding.css
/* 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:

scroll-margin.css
/* 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 vs scroll-margin

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:

accessibility.css
/* 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:

Browse the gallery
gallery-snap.css
.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:

snap-indicators.js
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:

scroll-snap-stop.css
/* 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 */
}
⚠️
Limited browser support

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
best-practices.css
/* 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-type on the container to enable snap
  • scroll-snap-align on children to define alignment points
  • mandatory for strict control, proximity for more freedom
  • scroll-padding to 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!

🎨
Go further

Discover our templates with pre-integrated Scroll Snap in our effects library, ready to be used in your projects.