- Read the Tea Leaves Software and other dark arts, by Nolan Lawson
- Building a modern carousel with CSS scroll snap, smooth scrolling, and pinch-zoom
- CSS scroll snap
- scrollTo() with smooth scrolling
- Hiding the scrollbar (optional)
- Pinch-zoom
- Intrinsic sizing
- Accessibility check
- Compatibility check
- Conclusion
- Footnotes
- Create beautiful carousels with scroll snap CSS property
- CSS snap properties
- Creating a carousel
- Demo
Read the Tea Leaves Software and other dark arts, by Nolan Lawson
Building a modern carousel with CSS scroll snap, smooth scrolling, and pinch-zoom
Posted February 10, 2019 by Nolan Lawson in performance, Web. 16 Comments
Recently I had some fun implementing an image carousel for Pinafore. The requirements were pretty simple: users should be able to swipe horizontally through up to 4 images, and also pinch-zoom to get a closer look.
The finished product looks like this:
Often when you’re building something like this, it’s tempting to use an off-the-shelf solution. The problem is that this often adds a large dependency size, or the code is inflexible, or it’s framework-specific (React, Vue, etc.), or it may not be optimized for performance and accessibility.
Come on, it’s 2019. Isn’t there a decent way to build a carousel with native browser APIs?
As it turns out, there is. My carousel implementation uses a few simple building blocks:
CSS scroll snap
Let’s start off with CSS scroll snap. This is what makes the scrollable element “snap” to a certain position as you scroll it.
The browser support is pretty good. The only trick is that you have to write one implementation for the modern scroll snap API (supported by Chrome and Safari), and another for the older scroll snap points API (supported by Firefox [1] ).
You can detect support using @supports (scroll-snap-align: start) . As usual for iOS Safari, you’ll also need to add -webkit-overflow-scrolling: touch to make the element scrollable.
But lo and behold, we now have the world’s simplest carousel implementation. It doesn’t even require JavaScript – just HTML and CSS!
Note: for best results, you may want to view the above pen in full mode.
The benefit of having all this “snapping” logic inside of CSS rather than JavaScript is that the browser is doing the heavy lifting. We don’t have to use touchmove listeners or requestAnimationFrame to try to get the pixel-perfect snapping behavior with the right animation curve – the browser handles all of it for us, in native code.
And unlike touchmove , this scroll-snapping works for any method of scrolling – touchpad, touchscreen, scrollbar, you name it.
scrollTo() with smooth scrolling
The next piece of the puzzle is that most carousels have little indicator buttons that let you navigate between the items in the list.
For this, we will need a little bit of JavaScript. We can use the scrollTo() API with , which tells the browser to smoothly scroll to a given offset:
function scrollToItem(itemPosition, numItems, scroller) < scroller.scrollTo(< scrollLeft: Math.floor( scroller.scrollWidth * (itemPosition / numItems) ), behavior: 'smooth' >) >
The only trick here is that Safari doesn’t support smooth scroll behavior and Edge doesn’t support scrollTo() at all. But we can detect support and fall back to a JavaScript implementation, such as this one.
Here is my technique for detecting native smooth scrolling:
function testSupportsSmoothScroll () < var supports = false try < var div = document.createElement('div') div.scrollTo(< top: 0, get behavior () < supports = true return 'smooth' >>) > catch (err) <> return supports >
Being careful to set aria-label s and aria-pressed states for the buttons, and adding a debounced scroll listener to update the pressed state as the user scrolls, we end up with something like this:
You can also add generic “go left” and “go right” buttons; the principle is the same.
Hiding the scrollbar (optional)
Now, the next piece of the puzzle is that most carousels don’t have a scrollbar, and depending on the browser and OS, you might not like how the scrollbar appears.
Also, our carousel already includes all the buttons needed to scroll left and right, so it effectively has its own scrollbar. So we can consider removing the native one.
To accomplish this, we can start with overflow-x: auto rather than overflow-x: scroll , which ensures that at least if there’s only one image (and thus no possibility of scrolling left or right), the scrollbar doesn’t show.
Beyond that, we may be tempted to add overflow-x: hidden , but this actually makes the list entirely unscrollable. Bummer.
So we can use a little hack instead. Here is some CSS to remove the scrollbar, which works in Chrome, Edge, Firefox, and Safari:
.scroll < scrollbar-width: none; -ms-overflow-style: none; >.scroll::-webkit-scrollbar
And it works! The scrollbar is gone:
Admittedly, though, this is a bit icky. The only standards-based CSS here is scrollbar-width, which is currently only supported by Firefox. The -webkit-scrollbar hack is for Chrome and Safari, and the -ms-overflow-style hack is for Edge/IE.
So if you don’t like vendor-specific CSS, or if you think scrollbars are better for accessibility, then you can just keep the scrollbar around. Follow your heart!
Pinch-zoom
For pinch-zooming, this is one case where I allowed myself an indulgence: I use the element from Google Chrome Labs.
I like it because it’s extremely small (5.2kB minified) and it uses Pointer Events under the hood, meaning it supports mobile touchscreens, touchpads, touchscreen laptops, and any device that supports pinch-zooming.
However, this element isn’t totally compatible with a scrollable list, because dragging your finger left and right causes the image to move left and right, rather than scroll left and right.
I thought this was actually a nice touch, though, since it allows you to choose which part of the image to zoom in on. So I decided to keep it.
To make this work inside a scrollable carousel, though, I decided to add a separate mode for zooming. You have to tap the magnifying glass to enable zooming, at which point dragging your finger moves the image itself rather than the carousel.
Toggling the pinch-zoom mode was as simple as removing or adding the element to toggle it [2] . I also decided to add some explicit “zoom in” and “zoom out” buttons for the benefit of users who don’t have a device that supports pinch-zooming.
Of course, I could have implemented this myself using raw Pointer Events, but offers a small footprint, a nice API, and good browser compatibility (e.g. on iOS Safari, where Pointer Events are not supported). So it felt like a worthy addition.
Intrinsic sizing
The last piece of the puzzle (I promise!) is a way to keep the images from doing a re-layout when they load. This can lead to janky-looking reflows, especially on slow connections.
Assuming we know the dimensions of the images in advance, we can fix this by using the intrinsicsize attribute. Unfortunately this isn’t supported in any browser yet, but it’s coming soon to Chrome! And it’s way easier than any other (hacky) solution you may think of.
Here it is in Chrome 72 with the “experimental web platform features” flag enabled:
Notice that the buttons don’t jump around while the image loads. Much nicer!
Accessibility check
Looking over the WAI Carousel Concepts document, there are a few good things to keep in mind when implementing this carousel:
- To make the carousel more keyboard-navigable, you may add keyboard shortcuts, for instance ← and → to navigate left and right. (Note though that a scrollable horizontal list can already be focused and scrolled with the keyboard.)
- Use
and - elements instead of s, so that a screen reader announces it as a list.
- The smooth-scrolling can be distracting or nausea-inducing for some folks, so respect prefers-reduced-motion or provide an option to turn it off.
- As mentioned previously, use aria-label and aria-pressed for the indicator buttons.
Compatibility check
But what about IE support? I can hear your boss screaming at you already.
If you’re one of the unfortunate souls who still has to maintain IE11 support, rest assured: a scroll-snap carousel is just a normal horizontal-scrolling list on IE. It doesn’t work exactly the same, but hey, does it need to? IE11 users probably don’t expect the best anymore.
Conclusion
So that’s it! I decided not to publish this as a library, and I’m leaving the pinch-zooming and intrinsic sizing as an exercise for the reader. I think the core building blocks are simple enough that folks really ought to just take the native APIs and run with them.
Any decisions I could bake into a library would only limit the flexibility of the carousel, and leave its users high-and-dry when they need to tweak something, because I’ve taught them how to use my library instead of the native browser API.
At some point, it’s just better to go native.
Footnotes
1. For whatever reason, I couldn’t get the old scroll snap points spec to work in Edge. Sarah Drasner apparently ran into the same issue. On the bright side, though, a horizontally scrolling list without snap points is just a regular horizontally scrolling list!
2. The first version of this blog post recommended using pointer-events: none to toggle the zoom mode on or off. It turns out that this breaks right-clicking to download an image. So it seems better to just remove or add the element to toggle it.
Create beautiful carousels with scroll snap CSS property
You can create beautiful carousels with a fancy and smooth snap effect by implementing a CSS-only solution.
The scroll snap effect means that each time a user finishes scrolling an element will be snapped to the edge of the scroll container.
This effect looks especially good on mobile devices, both for vertical and horizontal scrolling.
CSS snap properties
To add a basic scroll snap for a container with the scroll you’ll need to define one single property.
.scroll-container scroll-snap-type: x mandatory; >
The scroll-snap-type CSS property sets how strictly snap points are enforced on the scroll container in case there is one.
There are a few options available for the scroll-snap-type property.
To specify the axis use x — horizontal or y — vertical value, and for behavior use mandatory — will always snap to the next element after the scroll action, proximity — will snap to the next element when the scroll ends in the near proximity of a snap point.
For the child elements, you should specify the scroll-snap-align which will set the snap position of the element. The values can be start , end , or center .
.scroll-child scroll-snap-align: start; >
You can apply additional properties to customize the scroll behavior.
For the scroll container, you can add a scroll-padding . This property will create an offset by the edge of the scroll container. It comes in handy when there is an overlying element, like a sticky header.
.scroll-container scroll-snap-type: y mandatory; scroll-padding: 30px; >
For child elements, you can add a scroll-margin . This property represents the outset from the corresponding edge of the scroll container.
.scroll-child scroll-snap-align: start; scroll-margin: 10px; >
The scroll snap properties are fully supported by all modern browsers:
Creating a carousel
For this example, we’ll create a small horizontal carousel that will showcase a list of cards with programming courses.
The HTML structure of a carousel is pretty simple. A div container with child elements.
class="carousel"> class="carousel-item">