Carousel using React Hooks and CSS scroll snap
We all know how difficult it is to build an accessible, user friendly and mantainable carousel, specially from scratch. Well, for a project I was working on, I needed to build one. The carousel specs also included a scroll indicator, arrow buttons to go back and forward trought for desktop devices, but support touch scroll support in mobile, and finally, it needed to be performant and accessible.
At this point, a great option would have been to use one of the many great react-carousel libraries out there and that could have been it. End of this entry. But I also took this as a great opportunity to try and build one from scratch and fully understand how to develop it using only modern css and React. I mean, If that hadn't gone well, I could always go back and use a library, right?
But let's fully dive in and start the coding. We are gonna try make this component as reusable as possible. So let's take a look at our base structure first:
CSS Scroll Snap
I told you we will use modern CSS. This CSS property provide us a way to get more control over the scroll experience by setting different snap positions.
scroll-snap-type
We use this property in our container to specify the direction and the behavior of the scroll.
CSS Scroll Snap on developer.mozilla.org
scroll-snap-align
This is for all cards and is used to define the alignment which could be set as center
, start
, and end
. For this example, we are gonna use start
.
To keep the ability of allowing all types of childs outside the component, we are gonna use the > *
selector to style only all the direct children of .container
Let's add some other key styles to the carousel.
Scroll indicator
We'll work on a seperate component and use useEffect
and a scroll event listener. Also, useRef
will be needed to prevent a re-render of our whole component each time the scroll listener is triggered.
As we are passing a ref from the parent component, we need to use forwardRef
in the ScrollIndicator
component.
[...Array(count).keys()]
is used to create an array based on the count
number but also setting the keys as values. e.g. count = 2
array = [0, 1]
Inside the ScrollIndicator
component we need to add a local state for the scroll progress and some way to update it each time the user scrolls on the carousel. For this we are gonna use useState
, useEffect
and the sliderRef
.
To break it down,
- First, the
carouselRef
is used to read the element properties and calculate the state of the scroll. element.scrollLeft
is the position of the scrollbar from the leftmost point of the container.element.scrollWidth - element.clientWidth
is the maximum position for the scrollbar to scroll based on the total width of the container.(windowScroll / totalWidth) * 100)
is the percentage scrolled by the user.- We are attaching this whole function to the
scroll
event at the end of theuseEffect
- And finally, we are using the
const activeDot = Math.floor((scrollProgress * count) / 110)
to calculate the active dot based on thescrollProgress
and the total number of elements.
Now, we can add an active
class and bind it to const activeDot
.
Scroll to buttons
This part is a bit Tricky and unfortunately does not have the greatest browser Support Yet, but nothing that a polyfill can't solve.
Inside our parent element, we need to add two buttons and the onClick function for each of them.
We are using scrollBy()
to scroll inside our element and carouselRef.current.clientWidth
to read the width of the screen.
Caveats
At the point I'm writing this post scrollBy
and smooth
scroll property aren't highly supported by all the browser but you can use some polyfill to easily solve this problem.