Development,

2019-10-04

My React Carousel

These days it's a very common design pattern to use carousels to present content that is interesting but maybe not crucial for the user to see. We often see this in image feeds on Instagram, campaigns on almost any e-commerce or during onboarding in mobile apps.

While this post is not going to be about pros and cons with carousels, there are some interesting posts about this that I really recommend.

Image Carousels and Sliders? Don't Use Them. (Here's why.) https://conversionxl.com/blog/dont-use-automatic-image-sliders-or-carousels/

The Pros and Cons of Content Carousels https://www.workhorsemkt.com/pros-and-cons-content-carousels/

Background

When I got the task of building an Instagram feed for our client Flattered, I took a deep breath and started googling "react carousel" like so many times before. Believe me when I say that there are many different libraries out there, each one with its own pros and cons. There are heavy does-it-all libraries that ship the whole world in their bundles, there's lightweight ones that look really cool but are impossible to style, and there's the one that's really amazing but just lacks that one feature that you really needed.

After some hours of still not finding the perfect one I said to myself:

- Hey, what if I would just build my own carousel? I mean, how hard can it be?

via GIPHY

Idea

For the specific case I built the carousel I had some requirements:

  • It should be possible to display several images at the same time
  • The amount should be dynamic so I can have e.g. 5 images on desktop and 1 on mobile
  • The carousel should be infinite, meaning when you come to the end you should start over
  • There should be previous/next-buttons
  • It has to fill the width of its container
  • It should support swiping on mobile and desktop

I also decided to give myself some additional constrains based on my experience of trying out other React carousels:

  • It should have no dependencies (except React of course)
  • It shouldn't require that you import any CSS
  • The API should be really simple, ideally just wrap your images in a component

Trying to fulfill all of this actually turned out to be easier that I thought!

First Steps

I started out by figuring out what my dream API would look like. I pretty much started and ended up with:

import Carousel from 'my-react-carousel'

const App = () => (
  <Carousel>
    <div>My first slide</div>
    <div>My second slide</div>
    <div>My third slide</div>
    <div>My fourth slide</div>
    <div>My fifth slide</div>
  </Carousel>
)

I then created the Carousel component and set it up to render a container that used flexbox to render its children. I realised I had to set the width of each slide to (1 / slidesToShow) * 100 percent, and to do that I needed to somehow set the style prop on every one of them. There are some really handy functions provided by React to do this - React.cloneElement and React.Children.map. Check them out at the React Top-Level API documentation.

Infinite Swiping

I had to think for a bit before I got this part right. The basic concept behind a carousel is that it is a container element that has many children and it only displays the ones that fit inside the visible width. Usually this means that there is a scroll bar in the container element that allows you to scroll other things into view. In the case of our carousel we hide the scroll bar, and instead programatically scroll the user to the correct position when they press the next/previous buttons. So far so good.

When we want to implement infinite swiping, we have to be a bit more creative. Infinite swiping means that when you reach the end of the carousel (the fifth slide in our example above), you should seamlessly continue seeing the slides starting from the first one, just like how it is in a real carousel. Since the current web is built more like a flat earth rather than a round globe, we have to create this behaviour ourselves.

Let's say that we have 5 slides in total, and that 3 of them fit in the same view. This means that when we have slide 1, 2 or 3 as the left-most one, we're good. However, when we move to number 4, we would see 4, 5 and a blank spot. In the case of an infinite carousel, we would want to see number 1 there. Luckily, we can just clone slide 1, add it to the end of the list and we're all right!...

Until we move to the next slide of course. Then we'll see 5, 1 and a blank spot. So we need to clone slide 2 as well. Until we realise that when we move forward again, we see 1, 2 and a blank spot. See the problem?

Luckily, we're now at the end of this cloning. When we also clone slide 3, we end up with the following slides: 1, 2, 3, 4, 5, 1, 2, 3. What if we could just jump back to the real slide 1 when we have reached the clone? Wouldn't that make it feel infinite?

Yup. That's what I did. And it works.

Animations

The transition between slides is very important in a carousel. I'm using regular CSS transitions to animate between different states, using transform to determine what slides are in the view. The tricky part comes when you want to make the jump between the clone and the real slide, when you reach the edge of the world. I had to implement a state where, during the jump, the animation was disabled and then re-enabled again. This taught me the importance of using requestAnimationFrame because otherwise things would not sync correctly in some browsers.

my-react-carousel

An Instagram carousel at Flattered using my-react-carousel.

Touch and Mouse Swiping

Initially I only had two buttons that changed to the next/previous slide by calling a function exposed by useImperativeHandle on the carousel component. However, on mobile it is much more natural to just swipe the slides so I had to implement touch and mouse swiping. This was mostly a matter of creating methods to handle onTouchStart, onTouchMove and onTouchEnd that keeps track on how far you've moved since you started and that will calculate what slide you're on when you let it go.

Preventing Vertical Scroll

One annoying feature of some carousels is that when you swipe to scroll the carousel you sometimes also scroll vertically on the page. There's a very easy solution for this that I implemented in the carousel, just add overflow: hidden to the body-tag whenever you're swiping the carousel and it will just work! Definitely something that mobile users will appreciate!

  • Later on I found out that this does not work as intended for iOS. There are workarounds though.

Then Came All the Feature Requests

When I had finished my first implementation of the carousel and was really satisfied with my work, I got to give some feedback on the design work we've done for another client. Starting out really excited I saw that there was a few places where My React Carousel could be used and I was so ready to get started on implementing it, only to figure out that in this case, they wanted me to implement visual indicators in the form of little circles to indicate which slide you're on and how many there are.

My dream of My Perfect React Carousel was crushed in an instant. I didn't want to bloat the carousel by adding built-in dots that would be super hard to style the way you want them. I didn't want to spoil my beautiful API with some crazy render props stuff, or having to move the slides from being children of my component to some stupid array or... I spent night and day desperately struggling to find a way out of this.

And finally I found it! The optional render prop API that gives you control of the data inside the component, while still maintaining the original API for ease of use.

my react carousel example2

My-React-Carousel used in Prima Trafikskola.

Render prop API

In React we normally see children being what's rendered from a component. In this case I'm using children as a source to feed the carousel, while using the prop render to optionally customize the output of the component.

This is a pattern I haven't really seen being used in other React components, so it feels a bit unnatural, but is a good way to create an incrementally flexible API.

By default, the following render function is used:

const defaultRenderer = ({ slides }) => slides

This means that the slides are returned as-is, just with added styling.

However we also provide other props in the render function which allows us to do stuff like this:

const buttonsRenderer = ({
  slides,
  currentStep,
  totalSteps,
  hasNext,
  next,
  hasPrevious,
  previous,
}) => (
  <>
    {slides}
    {hasPrevious && <button onClick={previous}>Previous</button>}
    <span>
      {currentStep} of {totalSteps}
    </span>
    {hasNext && <button onClick={next}>Next</button>}
  </>
)

With the API this exposes, adding dots is no problem!

End of Chapter One

I learned a lot from building this carousel, and since the first iteration I've added some more features, and I'm sure there will be more feature requests coming soon.

The requirements and constraints I put on myself when building this package made it really fun and challenging, and forced me to learn a lot of new stuff, and be creative in how I can expand the API while maintaining the parts I liked to much. It's far from perfect, but I think it's an interesting way of creating components that are easy to use, yet powerful and customisable.

The result of this journey can be found at the npm registry in addition to the source and documentation on Github.

Written by Alfred Ringstad

Previous