Complete Guide to CSS Scroll Snap

If you have ever tried to make a carousel or slideshow of images on your site, you have probably ended up using one of many Javascript libraries to do it. The introduction of CSS scroll snap has made creation of carousels like this a lot easier. Scroll snap can also be used to easily create landing pages which snap to each section when scrolling.

Scroll snap is a pretty new feature, and recently there has been a lot of change in its implementation. In this post, I’ll outline how it works, and how to use it. At the end of this post is a demo that works in all major browsers.

Before we get to the demo though, let’s take a look at the state of the specification, and how to use it.

Multiple specs

Scroll snap was originally specified back in 2015. At that point it was implemented in Safari and Firefox. Edge and IE also partially support this spec (with the -ms prefix), but Chrome never implemented it at all. In 2018, the spec was heavily revised, including changes that are not compatible with the 2015 spec. A lot of posts online refer to this old spec, and even MDN is yet to be updated with the 2018 spec. In this post I will be focusing on the new spec.

It is important to note that as of writing this, the 2018 spec is not fully finalised, and some small details may change (especially scroll-snap-stop, discussed later).

Current Browser Support

Chrome 69 and Safari 11 both support the new spec. It’s currently unclear when the other browsers will update to the newer spec, so for the widest support, you need to write code for both specs. For simple cases this is not difficult, but for more advanced features, this can be a pain.

As always, CanIUse has a great support table for scroll snap, including which version of the spec each browser supports.

The Basics

To implement scroll snapping with the 2018 spec, we need two things: a container which scrolls, and elements inside it which we can snap to.

The Container

Assuming we have a scrolling container, setting it up for snapping is simple. We use the scroll-snap-type property to do this.

First we need to specify the axis which we want to snap. There are five possible options:

x: Only snap when scrolling horizontally (x-axis)

y: Only snap when scrolling vertically (y-axis)

both: Snap when scrolling either horizontally or vertically

inline: Only snap when scrolling on the “inline” axis. For pretty much all cases, inline is the x-asis. (It’s called inline because elements with display: inline are normally placed horizontally)

block: Only snap when scrolling on the “block” axis. Similar to inline, this is essentially the same as y.

We also need to choose how the browser should choose to snap once the user has finished scrolling. This is called the strictness. There are three possible options.

mandatory: At the end of a scroll, the browser must snap to the nearest element.

proximity: At the end of a scroll, the browser may snap if it wants to, but is not required to.

none: No scroll snapping

Choosing a strictness can be tricky. For a carousel (like an image slideshow), mandatory is a good choice, because we always want an element to be snapped. In other cases, such as a landing page with large sections you want to snap to, proximity may be a better choice. Forcing snapping on all scrolls can be a jarring user experience. In fact, the spec warns to think carefully before choosing mandatory.

Let’s say we want to make a simple carousel which scrolls horizontally and snaps to each element. Assuming our carousel container is already set up to scroll horizontally, all we need to add one line of CSS.

#carousel {
scroll-snap-type: x mandatory;
}

The Snappable Elements

Now that the container is set up to snap, it is up to the inner elements themselves to specify how they should be snapped. For this, the scroll-snap-align property is used. When the browser decides to snap to an element, it uses this property to decide where to scroll to. There are four options to choose from here:

start: The element will be aligned to the start of the container. That is, it will snap it to the left if the axis is x, and top for y.

center: The element will be snapped to the centre of the container.

end: The element will be snapped to the right of the container if the axis is x, or bottom if y.

none: This element will not be considered for snapping. Instead, the browser will try to snap to the next closest element.

If you are unsure which to choose, center is usually the best option. For our carousel, it will definitely look best.

#carousel >div {
scroll-snap-align: center;
}

Further snap control

To have a bit more control over where and when the browser decides to snap, you can set scroll-padding on the container, or scroll-margin on the elements inside. These shorthands are written the same way as normal padding and margin, and also have the longhand versions, such as scroll-padding-top.

scroll-padding

Padding defines the optimal viewing area of the container. It does not actually change how anything looks, but the browser uses it to determine where the best point to snap is. You can use this to tell the browser about areas that may not be visible to the user. For example, if you have a 40px toolbar which overlays the top of your container, you should set this:

#carousel {
scroll-padding-top: 40px;
}

Now when the browser is snapping, it will attempt to snap in such a way that the element which has been snapped to is not covered by this toolbar.

scroll-margin

The scroll-margin property allows you to adjust the inner elements’ scroll snap area. In theory, you can use this to make some elements more likely to snap than others. But in practice, how this is used is very much browser-dependent.

For example, say that you want the browser to snap forward easily, but require much more scrolling to go back. To do this, set up the scroll-margin so that each element’s snap area is shifted 50% to the left, thus making elements on the right more likely to snap.

#carousel >div {
scroll-margin: 050%0-50%;
}

scroll-snap-stop

Many modern browsers, especially on mobile devices, support “momentum scrolling”, where the user can scroll very fast with one big swipe. In this case, the browser would skip over many potential snap points. The scroll-snap-stop property allows an inner element to define itself as unskippable, forcing the scroll to stop, even if the momentum of the scroll would take it much further.

#carousel >div.unskippable {
scroll-snap-stop: always;
}

In the current draft specification, it is noted that this property may be removed from the final spec. Presumably this is because this forced scroll stopping could end up being very annoying behaviour for users.

Demo

Let’s make a simple carousel using CSS scroll snap. Take a look at the demo below (make sure to click the Result tab if it is not open). Scroll or swipe to see the snapping in action. Also click the buttons to see how easy it is to change between a horizontal and vertical carousel.

This demo supports both versions of the spec, and so will work on Safari 11, Chrome 69, Firefox 39, and Edge 12+. It could even be made to work in IE 10+ if flexbox was not used.

Try playing around with the demo on Codepen, and see how the different features work. For example, try setting scroll-snap-align: none on #carousel-2, and notice how the browser will no longer snap to it, instead skipping over to the next closest item.

Ready to Use?

Though the specification is not completely finished, it is hard to see any major changes since scroll snap has already been implemented by Chrome, the most popular browser. Now that Chrome 69 has been released, all major browsers support some form of scroll snapping. Unfortunately there are currently no polyfills for the 2018 spec, so for now you will need to support both versions of the spec in your code. As you can see in the Demo, this is not hugely difficult, and you gain easy scroll snapping in all browsers natively this way.

The fallback for lack of support is graceful, simply becoming a normal scrollbar, so there is not much downside to implementing it if you need it.