Search justacoding.blog. [Enter] to search. Click anywhere to close.

November 11th, 2021

Build A React Carousel Component With Infinite Scroll

The carousel is a classic piece of functionality. It’s a mechanism that the user can utilize to scroll through slides of content, image-based or otherwise.

Here’s the accompanying infinitely-scrolling React carousel example (accessible directly in JSFiddle).

This carousel example provided is very much a “bare-bones” implementation. I’ve tried to capture the core functionalities in the most simplistic and easily digestible manner. The carousel component is animated, and it gives the impression of having an infinite scroll in either direction.

Let’s break down how the carousel works.

The configuration, an array of slides

As you can see, this particular React carousel example expects a prop called slides – this should be an array of simple JavaScript objects.

These slide objects look like so:

{
    title: "Slide #1",
    content: () => <p>I am the content for the first slide</p>
}

As expected, these slide objects define what content the carousel will actually display.

This implementation allows functions and components to be passed in as configuration, meaning the content of the individual slides is flexible and can be easily used for many different purposes (the user isn’t just limited to purely text based slides, for example).

Given an array of slides, we firstly need to indicate a visible slide before we can look at transitioning and animating to other slides in the sequence.

Marking a slide as “visible” or active

Our React carousel component utilizes a piece of state called visibleSlide. The value of visibleSlide is an index that represents the slide in the sequence which should be visible to the user.

This defaults to 1 instead of 0 for reasons I’ll go into later on.

const [visibleSlide, setVisibleSlide] = useState(1)

Monitoring the visible slide is the most important aspect of the carousel. Manipulating this value is the crux of the component itself and it’s what allows our carousel to actually do something (ie. scroll, automatically or otherwise).

This is used to determine which slide is the active slide. The active slide is the slide that’s currently visible within the carousel element.

Swapping to a different visible slide is as simple as calling the method returned via the relevant useState hook:

setVisibleSlide(3)

The various modes of the carousel component

This React carousel component example supports two modes, which we’ll refer to as manual mode and automatic mode.

Let’s take a look at the implementation details for each mode.

Manual mode

Manual mode allows the user to scroll through the carousel slides themselves via left and right arrow controls.

Scrolling to the left or right

Scrolling in either direction is as simple as incrementing or decrementing the visibleSlide value stored in state.

const scrollLeft = () => {
    setVisibleSlide(visibleSlide - 1)
}

const scrollRight = () => {
    setVisibleSlide(visibleSlide + 1)
}

We don’t need to worry about animations or transitions or anything else at this point. Just know that whatever index we pass to setVisibleSlide — the carousel component will treat this particular slide as the visible one.

(Note: we’ll go over what happens when you scroll out of bounds a bit later on in the article)

So it’s simply a matter of attaching the relevant functionalities to each button:

<a onClick={!leftAndRightDisabled ? scrollLeft : null} 
    href="javascript:;" 
    className={`scrollLeft ${leftAndRightDisabled ? "disabled" : ""}`}>
    Left
</a>

<a onClick={!leftAndRightDisabled ? scrollRight : null} 
    href="javascript:;" 
    className={`scrollRight ${leftAndRightDisabled ? "disabled" : ""}`}>
    Right
</a>

One function to scroll left on click, and one function to scroll right on click. We’ll go over some of the other elements portrayed in this particular snippet in due course.

Automatic mode

Automatic mode scrolls on it’s own.

The user doesn’t have access to any of the aforementioned controls (the left or right arrows) as we’re hiding them in this scenario. This is just to prevent the user from interfering with the automatic scrolling mechanism. The user can stop and start the carousel via dedicated controls when in this mode, instead, though.

As you may have guessed, actually scrolling to the left or right in automatic mode is, in principle, exactly the same as in manual mode. We just need to increment the visibleSlide value stored in state.

This particular React carousel example only supports automatic scrolling in one direction (left to right) so we’re only incrementing here, but it should be trivial enough to support automatic scrolling in reverse (where we’d decrement visibleSlide, instead).

Starting the carousel

To actual start the carousel and initiate the automatic scrolling, we can make use of setInterval:

const start = () => {
  intervalId.current = setInterval(() => {
      setVisibleSlide(prevVisibleSlide => {
      if (prevVisibleSlide + 1 === stateSlides.length) {
        return 0
      }
      return prevVisibleSlide + 1
    })
  }, speed)
}

I’m using useRef to store a reference to the interval ID here.

The reason for this is that we don’t want to have to manipulate state to clear this interval (which would stop the carousel). We want this operation to be instant (which setting state may not necessarily be, given it’s asynchronous nature).

So there’s no good reason to store this interval ID in state, and useRef is a better alternative in this scenario.

Stopping the carousel

We also have an accompanying function which is responsible for stopping the scrolling:

const stop = () => {
  clearInterval(intervalId.current)
}

It’s just a means of calling clearInterval and passing in the relevant interval ID that was set by out start function.

With those functions in place, it’s simply a matter of attaching them to buttons in the UI to allow the user to initiate the relevant functionalities directly:

<a onClick={start} href="javascript:;">Start</a>
<a onClick={stop} href="javascript:;">Stop</a>

These elements, combined, form the basis of the automatic mode implemented within this carousel example.

What’s prevVisibleSlide used for here?

You may have noticed that the start function refers to the previous visibleSlide value in order to determine the next index to scroll to.

This is a necessity, as setInterval retains only the initial value of visibleSlide once it’s invoked.

This means if we were to increment it directly, without referring to it’s previous value as we are doing, it’ll always be the same value. That’ll be a value of 1 — given the initial value is always 0.

Starting the automatic scroll right away

You may have notice a prop called autoScroll.

This is passed to the React carousel component itself, and it determines if the carousel should auto-scroll immediately or not.

If passed, we don’t supply any start or stop controls to the user. But we make the carousel play automatically right away, instead:

useEffect(() => {
  // ...  
  if (!!autoScroll) {
    start()
  }
}, [])

This is handled inside a useEffect hook, with an empty array passed dependency as the second parameter.

This functionality runs once. If the prop has been passed as required – we can initiate the automatic scrolling from here.

Actually animating the transitions

The transitions in this carousel example work in conjunction with the absolute and relative positioning structure that’s been provided:

animating the transitions in the carousel
The structure and positioning of the carousel elements allows us to move slides into the currently viewable area by adjusting the left margin

Position and structure of the carousel elements

There’s a “container” that has relative positioning. The underlying slide container nested within has an absolute positioning.

The slides positioned within this slide container are positioned alongside one another, in a row.

To show the next (or previous) slide in the carousel, it’s simply a means of adjusting the left margin of the slide container. This pushes the next (or previous) slide into the visible viewpoint.

The rest of the slides remain hidden due to overflow: hidden.

The actual animations

It’s as simple as applying a CSS transition class to the relevant element for this:

.transition {
  transition: ease all 0.5s;
}

As outlined above, the fundamental behaviour of this carousel component is controlled by adjusting margins (specifically, the left margin of the relevant element).

With the transition applied, instead of the slides snapping immediately into position — they will instead ease into position. This gives a nice effect that’s standard for these kind of carousel elements.

Adding infinite scroll to the carousel component

As you may have noticed, this carousel has infinite-scrolling functionality.

By this, I mean once the user surpasses the very last slide; they are returned back to the first slide. Likewise once the user navigates left from the starting slide; they’ll be returned to the last slide as expected.

This process can repeat itself indefinitely in one direction, without the need to animate back to the starting slide.

The problem with infinite scrolling when paired with animations or transitions

Imagine a carousel that is not animated in terms of one slide transitioning in to another.

In this scenario, without a scrolling animation, infinite scrolling is not an issue.

However, when slides actually scroll from one direction, it’s important to keep the continuity each time. So upon reaching the end of the sequence, it’s not really ideal to animate or skip right back to the start in the opposite direction.

This just looks a bit unsightly and it isn’t the aesthetic we are after.

The solution

To work around this, I decided to add “clone” slides to the sequence, like so:

useEffect(() => {
  const slidesWithClones = [...slides]
  slidesWithClones.unshift(slidesWithClones[slidesWithClones.length - 1])
  slidesWithClones.push(slidesWithClones[1])
  setStateSlides(slidesWithClones)
  
  // ...
}, [])

So a clone slide is applied to the start of the array (a clone of the last slide) and the end of the array (a clone of the first slide).

the carousel layout and structure
The carousel structure with both clone slides applied

The general idea with these clone slides is as follows:

  • the user reaches the end of the sequence
  • they scroll to the right once more
  • they’ll land on a clone slide now, which is a copy of the first slide
  • once the transition is done, disable the transition behaviour entirely and actually jump back to the (actual, real) first slide

So the point here is that we are obscuring the “snap” back to the start of the slider by awaiting the end of the transition on the clone slide, so it’s fully stationary — then setting our margins as required.

Then enabling the transitioning again.

We do the same in the reverse direction, too.

The overall effect of this is that the carousel can be continuously scrolled in either direction without resetting itself. That’s not the actual reality, however, as described above. The carousel is resetting itself — but that behaviour is effectively obscured from the user.

Preventing odd behaviour

I’ve used a boolean called setLeftAndRightDisabled in the component example provided.

const [leftAndRightDisabled, setLeftAndRightDisabled] = useState(false)

When we perform our slide swap functionality, so when navigating from a “clone” to it’s real counterpart — it’s necessary to disable the left and right controls (if in manual mode).

That’s because it’s possible for the user to spam-click the controls at this particular point in time, and this can unfortunately cause a little bit of chaos when executing our clone-specific behaviour.

There may be a better way around this issue, but this is the rudimentary approach I’ve taken for now.

Working with setTimeout and setInterval along with the (asynchronous) nature of setting state within React can often lead to issues and quirks that need to be manually rectified, such as this one.

Adding a slide indicator to assist with navigation

It’s nice to have some kind of indication with regards to how many slides exist within the React carousel.

To handle this, I’ve added what I will call the slide indicator.

It’s simply a series of dots. These dots represent the slides themselves, with the currently active (or visible) slide being indicated amongst these dots.

The functionality is implemented via simple map:

<div className="slideIndicator">
  {stateSlides.map((slide, index) => {
    if (index === 0 || index === stateSlides.length - 1) {
      return null
    }
    return <div 
      key={index} 
      onClick={() => setVisibleSlide(index)} 
      className={`dot ${dotIsActive(index) ? "active" : ""}`}>
    </div>
  })}
</div>

It’s possible to click these dots to scroll directly to the slide represented by that specific dot.

Obscuring the clone slides

Of course, we have 2 more slides than originally passed through to the React carousel component as props.

Specifically, the clone at the beginning and the clone at the end of the array.

We can obscure those slides from being represented via the simple logic as demonstrated in the snippet above.

Then it’s a matter of making sure the correct dot is activated when we would ordinarily activate one of the (now) obscured dots:

const dotIsActive = (index) => {
  return (
    index === visibleSlide || 
    (index === 1 && visibleSlide === stateSlides.length - 1) || 
    (index === stateSlides.length - 2 && visibleSlide === 0)
  )
}

Now the slide indicator looks and works as expected, the user doesn’t see or experience the specially-added clone slides in any way.

Adapting this carousel component example

This type of component can most definitely be adapted into many different forms.

Here are a few possible tweaks and adjustments you could make.

Show multiple slides at once

With some adjustments to the way the viewable window is handled, as well as some of the dimension calculations, it’d be possible to show the active slide in a central position with both slides (to the left and to the right of it) still visible.

In this scenario, it would be nice to fade out the non-active slides.

This is a common pattern used in many carousels, and it’d be fairly straightforward to implement alongside the functionalities provided in this particular example.

Add more configuration and customizability

Of course, there are a lot of different effects that can be applied to elements such as this one.

Why not add in some of these effects and allow them to be controlled via props or a specific config object?

Here are a few examples:

  • Apply an overlay to darken out background content
  • Change the transition animation from sidewards scrolling to something else
  • Optionally disable the animations entirely
  • Supply a mode whereby the carousel doesn’t infinitely loop; but once it reaches the end the “next” arrows disappears and the user must navigate back, instead

In closing

I hope this React carousel component example along with it’s infinite scrolling has been useful for you!

You can find other such examples of simple, reusable components (React and otherwise) in our Code Examples category.

Do check back later for more code examples, new ones are to be added on a regular basis!

Thanks for reading!

Have any questions? Ping me over at @justacodingblog
← Back to blog