A Simple Carousel with Scroll Indicator in React using hooks

Harshid
auquan
Published in
5 min readJul 22, 2020

At Auquan, we recently decided to redo our websites and optimize them for the mobile view. Our aim was to do some design changes for a better mobile experience and also clear out some tech debt along the way. We made a lot of structural changes in our code, adopted styled-components and eventually ended up building up a well defined react component library. This is important for us as we wanted to reuse many of these designs and components frequently in future.

One of the primary components we built is the carousel for mobile. This component is important as many sections of our websites have content laid out horizontally, which of course can’t be rendered properly on mobile. A carousel would let us fit that content nicely on a mobile screen. Here’s an example of what we ended up building which highlights our requirements:

A working example of our carousel

This GIF shows one of the applications of our carousel. As you can see, we had very simple requirements.

  • The carousel is mobile only and should work with touch/drag events
  • The content should scroll freely and not forcefully focus any of the content
  • The dots should highlight the amount of content scrolled

Once we were clear with our requirements, we went through some popular NPM packages for the carousel like react-slick, bootstrap carousel, react-responsive-carousel and so on. But these carousels did not allow free scrolling and it seemed like a low valued effort to modify them to our needs. Also some of these packages were filled with features which we did not need at all. Features like zoomable images, pane buttons, a variety of builtin scroll indicators, etc. would end up becoming bloat for us. So finally we decided to build the carousel ourselves.

Building the Component Structure

We wanted to keep the consumption of the component as simple as possible. Our requirement was that just wrapping <Carousel /> around some content should be sufficient to render the carousel. Which meant that our component had to wrap the children elements with proper CSS and had to handle the rendering of scroll indicators.

Keeping that in mind, we decided to wrap the children elements inside <CarouselContainer /> and use a separate component <ScrollIndicator /> to render the scroll dots. So this is what we ended up with:

<CarouselContainer/> and <MainContainer/> are just div elements with CSS applied to them using styled-components.

The <CarouselContainer/> is supposed to be a container of the elements in the carousel. Which means it should satisfy three main conditions:

  1. It should render its children elements horizontally
  2. It should not display the scrollbar
  3. It should have a track of the scroll amount

Points 1 and 2 are pretty straightforward with the use of CSS. We use flex to render the items horizontally with fixed width and auto overflow to get the scroll experience. So the final CSS of <CarouselContainer/> becomes:

Note: We used ::webkit-scrollbar property to hide the scrollbar, which is not supported in firefox and IE (ref) — so if browser compatibility is important to you then this is not the best way.

So far, this is just a horizontal list without a scrollbar. This is technically the functionality we need, but it won’t be a carousel without the scroll indicator.

The Scroll Indicator

Now coming to the scroll tracking bit of the carousel, we assigned a ref to the <CarouselContainer/> using the createRef() hook. This ref is then passed as a prop called target to the <ScrollIndicator/> component which contains the main logic to display the scroll indicator dots. target would be used to calculate the scrollProgress using the DOM scroll properties (better explained through code). Read more about react ref here

To start off, this is how the final return statement looks like for the <ScrollIndicator/> component:

<ScrollIndicatorContainer/> is again just a styled div which centers the scroll dots and aligns them horizontally.

renderDots() renders the active/inactive dots.

The idea here is to create a listener which listens to scroll/touch events and calculates the scroll progress which can then be used to render the dots. We save the scrollProgress in the local state:

const [scrollProgress, setScrollProgress] = useState(0);

And the listener which calculates the scroll progress:

To break it down,

  1. target is the ref we created for <CarouselContainer /> in the beginning using the createRef hook. We use this to get the element properties needed to calculate status of the scroll.
  2. const windowScroll = element.scrollLeft;
    This property gives us the position of the horizontal scrollbar from the leftmost point of the container.
  3. const totalWidth = element.scrollWidth — element.clientWidth;
    This gives us the total width of the container where the scrollbar can scroll to.

Finally with these three values, we can calculate the scroll progress in percentage and set it in the state:

setScrollProgress((windowScroll / totalWidth) * 100);

Note: For vertical scrollbars, replace scrollLeft, scrollWidth and clientWidth with scrollTop, scrollHeight and clientHeight

When do we trigger the calculation?

Our function to calculate the scroll progress is ready but we still need to trigger it repeatedly to keep updating the scroll progress. We use the useEffect hook in order to bind this function with an event listener. For our use case this was mobile only so we binded it with the touchmove event, but we can bind this with scroll events as well for desktop usage.

This binding will make sure whenever the user touches and drags the <CarouselContainer/> and scrollListener() will update the scroll status for our use.

Now for the easiest part

Rendering the dots

This part would depend on what you want to show as a scroll indicator. For us, it’s a dot with a prop to highlight it.

The <Dot/> component is exactly what it sounds like, it renders a dot (it’s an HTML <div/> element with background color and border-radius). The active prop decides its color but that’s it. We use count prop here to decide the number of dots. Rest is just some math to decide which dot should be highlighted.

That’s it! The carousel is ready. Today we are using this component in multiple places for our mobile web pages.

Is this reinventing the wheel? Probably. But there are a lot of pros of having components which are built in house. We avoided new dependencies and some potential bloat. This is also highly customizable. We can add sliders, pane buttons etc. with our design choices. At the end of it, we believed the pros outweighed the cons and we are really happy with the end results.

Give it a shot! It’s simple and lightweight. If you come across any issues in the code snippets above please feel free to reach out to me!

--

--