Make your own card carousel in React 
Tags: react, tutorial, css, javascript
Published: 2021-07-30 09:39:05

When you have any sort of group of similar things, when it comes to presenting them you have a number of choices. You can use grids, tables, flexbox, they all do the job, but maybe you want to add a little bit of style and responsiveness to your page? If so, lets create our own carousel, a group of items we can swipe through with pointer events to add some interactivity. In the age of Instagram and Tinder, who doesn't like to swipe?

### Contents
- [Making a Carousel](#carousel)
- [(Optional) Adding Mouse Events](#mouse-events)
- [(Optional) Carousel Pagination](#pagination)
- [(Optional) Make it Mobile Friendly](#mobile-view)

To make a functioning carousel you may only need to complete the first section, and then you'll have all you need to take it further yourself. I have added basic CSS to the layout and won't be adding all of the styles here to keep things concise, but you may check it out in the project [repository](www.github.com/siaust/react-carousel) and import it to match the styles seen.
NB: I'll use ellipses (...) to signify removed code in some parts to shorten code blocks.

## Setting up the carousel <a name="carousel"></a>

First thing we need is some data, which we will populate our cards with. Lets keep it simple, we can use a Javascipt array of objects and import them into our main app.js. Here's an example of some data, by all means edit or add your own touch to this.

```javascript
export const data = [
  {
    name: "simon",
    img: "https://imgur.com/c43aAlv.jpg",
  },
  {
    name: "neo",
    img: "https://imgur.com/RF2a3PB.jpg",
  },
  {
    name: "morpheus",
    img: "https://imgur.com/B0SNpZI.jpg",
  },
  {
    name: "trinity",
    img: "https://imgur.com/KnXHM0K.jpg",
  },
];

```
Here we have a small array with some objects that have a `name` and `img` property. We will use these to populate the cards in the carousel later on. 

In our `App.js` file we can add an import for the data like so - `import {data} from "./data"` - ready for later. This is a named import, so make sure to get the variable name matching your export variable. Onward to building our carousel!

### Building the components of the carousel

First of all we need to make a component that will sit inside our carousel, the object which will slide across the screen. In this case I will call it a card and create it as a React component as so - 

```js
const Card = ({ name, img }) => {
  return (
    <div className="card">
      <img src={img} alt={name} />
      <h2>{name}</h2>
    </div>
  );
};

export default Card;
```
A simple component just holds two items, an image and a heading for our object name property. You can see the props are passed down into this component, lets set that up now from our `data.js`.


In `App.js` we will iterate over the data using the `map()` function and populate our root element with cards -
```javascript
import "./App.css";

import Card from "./components/Card";
import { data } from "./data";

function App() {
  return (
    <div className="App">
      <div className="container">
        {data.map((person) => {
          return <Card {...person} />;
        })}
      </div>
    </div>
  );
}

export default App;
```
We're using the `map()` function to iterate over the data and create a new `Card` for each person, passing in the properties using the spread operator. We already know the names of these properties match the component arguments, but it's one thing to look out for if your card doesn't display as you expect.

Now you should have something that looks like this (as long as you applied the `App.css` from the repo) -
![group of person cards in React project](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/udrih725wxhyybuj881l.jpg)

### Positioning the cards
Now we need to work on our carousel. In a carousel cards typically slide from the right or left, so we need to position our cards in some order, lets say "prevCard" on the left, "nextCard" to the right. These will be CSS classes we give the cards depending on their position.

Firstly we will add `position: absolute` to the `card` CSS class, this stacks all our cards on top of each other. Now we create some new CSS styles `prevCard`, `activeCard` and `nextCard` -

```css
.prevCard {
  left: 0;
}

.activeCard {
  left: 50%;
  transform: translateX(-50%); /* the card is centered 
                               /* to itself
                               /* instead of the left edge
                               /* resting on the center line
}

.nextCard {
  right: 0;
}
```
The next question is under what condition do we apply those styles to the cards? Well in the `map()` function we can add a parameter to read the current iteration index, `data.map((person, index) => {})`. This gives us the ability to apply the styles depending on a condition. But what condition? For example, we can say any card greater than index equal to zero should have the style `nextCard` applied. Lets look at the Javascript for this -

```js
{data.map((person, index) => {
    let position = index > 0 ? "nextCard" : index === 0 ? 
        "activeCard" : "prevCard";
    return <Card {...person} cardStyle={position} />;
})}
```
We're using nested ternary operators here to check the index and apply a style to the card, which we pass down as a prop. We also need to update the card component to take a new parameter `cardStyle` and apply that to the `className` attribute. We can use a template string to concatinate the new style with our required `card` style like so -

```js
const Card = ({ name, img, cardStyle }) => {
  return (
    <div className={`card ${cardStyle}`}>
...
```

If you save the app you may now see something like this -
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ae437ohikpjwztd94wqp.jpg)

Using Chrome dev tools and highlighting the container element, the problem here is that the `nextCard` "card" is positioning itself to its nearest positioned relative, of which there are none, so in this case is it the root element. We need to add a container, which will hold the cards and allow us to position them where we want.

```css
// css
.card-container {
  position: relative;

  width: 36rem;
  height: 22rem;
}
```
For simplicity sake, we're setting the width of the card container to three cards width, accounting for margin. This will allow a nice transition later on.
```js
// App.js
<div className="container">
    <div className="card-container"> /* wrap cards */
        {data.map((person, index) => {
            let position =
                index > 0 ? "nextCard" : index === 0 ? 
                "activeCard" : "prevCard";
            return <Card {...person} cardStyle={position} />;
        })}
    </div>
</div>
```
So we've positioned our cards, we can now add some controls to move them. Lets just use FontAwesome icons for this. You can find instructions for using FontAwesome and React [here](https://fontawesome.com/v5.15/how-to-use/on-the-web/using-with/react). We can simply use the `faChevronLeft` and `faChevronRight`. Once we've imported them, we can position them absolutely, and give them a `onclick` function, which we'll work on next.
```js
import { FontAwesomeIcon } from "@fortawesome/react
fontawesome";
import { faChevronLeft, faChevronRight} from "@fortawesome/free-solid-svg-icons";
```

### Carousel function
There is a glaring problem here. There is no previous card! This has been determined by our condition in the map function, so we need to fix this. At the same time, we can link in some functionality to our `onClick` handler, and also utilise `useState` hook from React. Lets break it down.

We need a starting point for our cards, an index, so we set up some state with a value of zero. We import the hook and declare our state variables -
```js
import {useState} from "react";

const [index, setIndex] = useState(0)
```
We are going to change this state value with our `onClick` functions and instead of comparing a hardcoded value `0`, we are going to compare the index of the map function with the state. This allows use to change the condition which applies the styles to the cards. First the functions -

```js
const slideLeft = () => {
    setIndex(index - 1);
};

const slideRight = () => {
    setIndex(index + 1);
};
```
Update the `FontAwesomeIcon` component with a `onClick` function -
```js
<FontAwesomeIcon
    onClick={slideLeft}
    className="leftBtn"
    icon={faChevronLeft}
/>
<FontAwesomeIcon
     onClick={slideRight}
     className="rightBtn"
     icon={faChevronRight}
/>
```
Finally the condition is updated to compare with the state value (updated the map index with a new name `n`) -
```js
{data.map((person, n) => {
    let position = n > index ? "nextCard" 
        : n === index ? "activeCard" : "prevCard";
    return <Card {...person} cardStyle={position} />;
})}
```
On testing I had some issues at this point with the transitions, and discovered this to be my mistake when using the position properties left and right with the CSS. It creates a smooth tranistion if you stick to the same property throughout, although this meant I had to use some tweaks to get the cards in the right places, using the CSS `calc()` function. The updated CSS for the cards -
```css
.prevCard {
    left: calc(0% + 2rem);
    opacity: 0;
}

.activeCard {
  left: 50%;
  transform: translateX(-50%);
}

.nextCard {
    left: 100%;
    transform: translateX(calc(-100% - 2rem));
    opacity: 0;
}
```
This will nicely position the cards left, center and right throughout the transition, accounting for the margin. Note the `opacity: 0` property, this is the result -
<img width="100%" style="width:100%" src="https://media.giphy.com/media/S8nN5ldfjDMjjgswO4/giphy.gif">
Here is with no change to opacity, so you may see easily what is happening -
<img width="100%" style="width:100%" src="https://media.giphy.com/media/bdLaHmyhq5SoHjYeae/giphy.gif">
Woo! Looks pretty nice! I'm sure you're already thinking of awesome ways to improve this, but firstly we just need to improve our function and stop changing the state if our index goes out of bounds to the data length. Otherwise, we could keep clicking forward, or backward for eternity, and the state would keep changing.
```js
 const slideLeft = () => {
    if (index - 1 >= 0) {
      setIndex(index - 1);
    }
  };

  const slideRight = () => {
    if (index + 1 <= data.length - 1) {
      setIndex(index + 1);
    }
  };
```
A couple of simple if conditions keep us within bounds and we can happily scroll left and right without a worry.

<hr>
## Adding Mouse Events <a name="mouse-events"></a>
Pointer events are things like a mouse clicking, dragging, moving over an element. We've already used one, `onClick`, in our FontAwesomeIcon component to trigger a card to move. Want would be nice is if we can click and drag, and pull the card across the screen. We can do this with some other [MouseEvent's](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent) that are available to us, like `onMouseDown`, `onMouseMove` and `onMouseUp`.

First we'll make a test function to see everything is working.
```js
const handleMouseDown = (e) => {
    console.log(e.target);
  };
```
Now we pass this function as a prop to our `Card` component and give the `onMouseDown` attribute this function in the container div.
```js
// App.js
<Card
    handleMouseDown={handleMouseDown}
    {...person}
    cardStyle={position}
/>
// Card.js
const Card = ({ handleMouseDown, name, img, cardStyle }) => {
    return (
        <div 
            className={`card ${cardStyle}`} 
            onMouseDown={handleMouseDown}>
...
```
Now if we click on a few cards we will see in the Chrome console something like -
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jdnopxh0q6kjqdbn0mm9.jpg)
On each click the event object is passed to our function which we use to log the `target`, which is the card. We can use the event to get the element we should move, the starting position of X, and use `document.onMouseMove` to track the cursors position. Once we have that, we can change the CSS `left` position property to reflect what the mouse does.

Firstly you may notice when dragging the card from the image it will be pulled along with your cursor. We need to stop this to prevent it interfering with our dragging of the card, we can do this in CSS by applying `pointer-events: none;` to the image. Other than that you may also be getting some selection happening when the mouse drags over the heading and image, to prevent that we can use `user-select: none` in the card class. An alternative if you want or need to allow selection is to have a specific area of the card as the draggable area, for this you would set your `onMouseDown` handler function to that particular element of the card, like a `<header>` or any other element you want.

So once that's sorted, now lets look at the function we need to track our mouse event -

```js
const handleMouseDown = (e) => {
    /* this is our card we will move */
    let card = e.target;
    /* to keep track of the value to offset the card left */
    let offset = 0;
    /* keeps the initial mouse click x value */
    let initialX = e.clientX;
    /* set the documents onmousemove event to use this function*/
    document.onmousemove = onMouseMove;
    /* sets the documents onmouseup event to use this function */
    document.onmouseup = onMouseUp;

    /* when the mouse moves we handle the event here */
    function onMouseMove(e) {
      /* set offset to the current position of the cursor,
      minus the initial starting position  */
      offset = e.clientX - initialX;

      /* set the left style property of the card to the offset 
      value */
      card.style.left = offset + "px";
    }

    function onMouseUp(e) {
      /* remove functions from event listeners
      (stop tracking mouse movements) */
      document.onmousemove = null;
      document.onmouseup = null;
    }
};
```
Now there's a few issues, sadly. First of all you'll immediately notice what feels like mouse lag. This is the transition CSS property on the card slowing down it's movement as it animates between positions. You can comment that out to fix it, but of course this will disable the nice animation when clicking the left/right chevrons. The second issue is that when we move the card `left` is instantly set to a pixel value and the card appears to jump left. This is definitely not what we want! We can fix both these problems by adding a(nother!) container around our card, which will take on the transition property and our card will be aligned within, so there will be no jump left.

First we wrap our card with a `<article>` tag, trying to follow HTML semantics, that will be what is positioned in the card container, and have the transition. The actual card will be absolutely position to this element, so when changing its `left` property, there won't be any oddness, as it hasn't previously been set.
```js
// Card.js
<article className={cardStyle}> /* class now applies here */
    <div className="card" onMouseDown={handleMouseDown}>
        <img src={img} alt={name} />
        <h2>{name}</h2>
    </div>
</article>
```
```css
article {
    position: absolute;
    width: 12rem;
    height: 100%;

    transition: all 1s; /* cut from .card class */
}
```
Now that the card is kind-of draggable, you will notice that the other cards, previous and next, are interfering when you drag the visible card near them. We fix this by adding a `<div>` with a sole purpose of "hiding" these elements, by using `z-index`. We create a div called, creatively, `background-block` and give it a `z-index: 0` and append our other elements accordingly. `prevCard` and `nextCard` get a `z-index: -1`.
```js
// App.js
<div className="card-container">
          <div className="background-block"></div>
          ...
```
```css
.background-block {
  position: absolute;
  width: 100%;
  height: 100%;
  z-index: 0;
}
```
This is what you should see -
<img width="100%" style="width:100%" src="https://media.giphy.com/media/C3xLm8scBn56ZJqiop/giphy.gif">
The last thing we need to do, the whole point of this, is to trigger the slide to the next or previous card. We go back to our `handleMouseDown` function for this, and add some conditions checking the value of `x`. Inside `onMouseMove` we add -
```js
function onMouseMove(e) {
    ...
    if (offset <= -100) {
        slideRight();
        return;
    }
    if (offset >= 100) {
        slideLeft();
        return;
    }
    ...
}
```
One last issue (I promise!), you'll notice that the cards retain the position after sliding back and forth. We can fix this by resetting their `left` property in the same block of code.
```js
if (offset <= -100) {
        slideRight();
        /* if we're at the last card, snap back to center */
        if (index === data.length - 1) {
          card.style.left = 0;
        } else {
          /* hide the shift back to center 
        until after the transition */
          setTimeout(() => {
            card.style.left = 0;
          }, 1000);
        }
        return;
      }
      if (offset >= 100) {
        slideLeft();
        /* if we're at the first card, snap back to center */
        if (index === 0) {
          card.style.left = 0;
        } else {
          /* hide the shift back to center 
        until after the transition */
          setTimeout(() => {
            card.style.left = 0;
          }, 1000);
        }
        return;
      }
```
Also, if the user releases the mouse before +- 100 pixels, the card will "stick", we can sort that in the `onMouseUp` function -
```js
function onMouseUp(e) {
    /* if user releases mouse early,
    card needs to snap back */
    if (offset < 0 && offset > -100) {
        card.style.left = 0;
    }
    if (offset > 0 && offset < 100) {
        card.style.left = 0;
    }
    ...
```
Actually, slight adjustments can be made to the style of `prevCard`; `left:0;` and `nextCard`; `transform: translateX(-100%);` - to keep a nice spacing after the change to wrapping with `<article>` element.

Et voila!
<img width="100%" style="width:100%" src="https://media.giphy.com/media/5LSBXAy910FjXqpDI2/giphy.gif">
<hr>
## Carousel Pagination <a name="pagination"></a>
Another optional thing we can do is add some visual feedback of where we are in the carousel. You can think of this as a form of pagination, although it's just for visual reference.

First we'll make a new component called `Paginator`. It will take two props, one is the length of the data, i.e. how many dots to represent the cards, and an index value which represents which card is active so we can style the respective dot to reflect this.

Here's our component -
```js
const Paginator = ({ dataLength, activeIndex }) => {
    let dots = [];
    let classes = "";
    for (let index = 0; index < dataLength; index++) {
        classes = activeIndex === index ? "dot active" : "dot";
        dots.push(<div key={index} className={classes}></div>);
    }
   
    return (
        <div className="paginator">
            <div className="hr"></div> {/* horizontal rule */}
            {dots.map((dot) => dot)}
        </div>
    );
};

export default Paginator;
```
You can see here we are using the `dataLength` to populate an array with JSX objects. One of those objects is give a class `active`, which will set it apart from the others. The CSS is straight forward and can be found in the repo (link at top).

In `App.js` we simply import our component and pass in the `data.length` and state value `index`. When we slide the carousel, the state value changes and the `Paginator` will receive this new value and render the updates accordingly.
```js
//App.js
...
<div className="card-container">
    <Paginator dataLength={data.length} activeIndex={index} />
...
```
<img width="100%" style="width:100%" src="https://media.giphy.com/media/23o8F0mOWAi9FtJzJ5/giphy.gif">
To make the dots clickable we can add a function to the `onClick` attribute like normal. We'll pass this function down from `App.js` into the `Paginator.js` component.
```js
//App.js
const handlePageChange = (page) => {
    let n = page - index;
    setIndex(index + n);
};
<Paginator
    ...
    handlePageChange={handlePageChange}
/>
//Paginator.js
onClick={() => handlePageChange(index)}
```
Basically the `onClick` function passing in a argument which is the index of the map function, for simplicity. This identifies what "page" it is, and we compare this with the state value. Then we can simply add the number (positive or negative) to set our index state and trigger a render.
<hr>
## Make it mobile friendly <a name="mobile-view"></a>
Earlier we added mouse events which handled clicking and dragging a card to trigger the functions which slide the cards. To make our carousel mobile friendly, we also need to add another kind of pointer event, called [TouchEvent's](https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent).

In our `Card` components `<article>` element we should add a new attribute `onTouchStart`. This event is fired when a tablet or phone has a finger or stylus touch the screen. We'll point it to the same function that handles our mouse events and make some changes there. We should also rename the argument to better reflect that it now handles pointer events, rather than just mouse events.
```js
// Card.js
<article className={cardStyle}>
      <div className="card" onMouseDown={handlePointerEvent} 
      onTouchStart={handlePointerEvent}>
...
```
In `App.js` we rename `handleMouseDown` to `handlePointerEvent` and then add a variable to check what type of event we're getting.
```js
let isTouchEvent = e.type === "touchstart" ? true : false;
```
We can use this flag a few more times when we are setting the X coordinate, again using ternary operators. Updating the code changes to -
```js
function onPointerEvent(e) {
    ...
    let initialX = isTouchEvent ? e.touches[0].clientX : 
        e.clientX;
    ...
    function onPointerMove(e) {
        ...
        offset = (isTouchEvent ? e.touches[0].clientX : 
            e.clientX) - initialX;
        ...
    }
...
}
```
You may notice that we're checking the first index of an array of the touch object. This is because many devices can use multi-touch, so you could track one or more fingers if you wished, for example using pinch to zoom. We don't need to track more than one though, so we just check the first, zeroth, finger/stylus.

We also need to add the functions to the documents touch event listeners, as we did before with the mouse events. We remove them when the touch ends, just like when the mouse click finished. This prevents our functions being called after we're done with them.
```js
// handlePointerEvent
document.ontouchmove = onPointerMove;
document.ontouchend = onPointerEnd;

// onPointerEnd
document.ontouchmove = null;
document.ontouchend = null;

```
Now if you check it out in Chrome dev tools with mobile view it works, but there is some issues when a card slides off screen to the right, expanding the view and causing scrollbars to appear briefly. We can fix this using media queries but hiding the overflow and restyling the elements slightly.
```css
@media screen and (max-width: 425px) {
  .container {
    width: 100%;
    overflow: hidden;
  }

  .card-container {
    width: 80%;
  }

  .prevCard {
    left: -35%;
  }

  .nextCard {
    left: 135%;
  }
}
```
This is just for one screen width of 425px and less, if you want to support more widths you'll need to do a bit more testing and add more media queries to reposition.

That's it! We've done it, a nice carousel, with touch and is responsive. Lets see the final product -
<img width="100%" style="width:100%" src="https://media.giphy.com/media/l0AC85Gc476hFTM0a5/giphy.gif">
<hr>
Phew, I hope you found some interesting things here and it helps you out. At the end of the day it's a basic carousel but by working through the process to create it I hope it gives you ideas of what else can be achieved. Thanks for reading! If you have any comments of suggestions please do add them below. 
[Cover photo by picjumbo.com from Pexels]( https://www.pexels.com/photo/closeup-photography-of-brown-and-white-carousel-225238/?utm_content=attributionCopyText&utm_medium=referral&utm_source=pexels)