How to create a carousel in React
Tags: react, javascript, carousel, css
Published: 2023-03-01 13:20:15

## Introduction

Carousels are a popular way to showcase multiple pieces of content in a limited amount of space. A carousel allows you to rotate through different items, such as images or product cards, using buttons or arrows. In this article, we'll explore how to create a carousel in React that is both animated and 3-dimensional.

This will involve leveraging React's powerful state management capabilities, as well as integrating CSS animations to create a smooth and dynamic user experience. By the end of this article, you'll have a functional and customizable carousel that can be easily integrated into your React-based projects. So let's get started!

## TLDR

Don't have time to read the full article and just want to see the full code, here it is

`App.tsx`

```tsx
import { useState } from "react";
import "./style.css";

import { useEffect, useRef } from "react";

// Based on "useInterval" from "react-use"
// https://github.com/streamich/react-use/blob/master/docs/useInterval.md

const useInterval = (
  callback: () => object | null | void,
  delay?: number | null
) => {
  const savedCallback = useRef<() => null | object | void>(() => null);

  useEffect(() => {
    savedCallback.current = callback;
  });

  useEffect(() => {
    if (delay !== null) {
      const interval = setInterval(() => savedCallback.current(), delay || 0);
      return () => clearInterval(interval);
    }

    return undefined;
  }, [delay]);
};

const Card = ({
  content,
  idx,
  onClick,
  onMouseEnter,
  onMouseLeave,
}: {
  content: string;
  idx: number;
  onClick: () => void;
  onMouseEnter: () => void;
  onMouseLeave: () => void;
}) => {
  let style = {};

  if (idx === 0)
    style = {
      opacity: 0.4,
      transform: "translateX(-40%) scale(0.8)",
      zIndex: 0,
    };
  if (idx === 1) style = { zIndex: 1 };
  if (idx === 2)
    style = {
      opacity: 0.4,
      transform: "translateX(40%) scale(0.8)",
      zIndex: 0,
    };

  return (
    <div
      className="card"
      style={style}
      onClick={onClick}
      onMouseEnter={onMouseEnter}
      onMouseLeave={onMouseLeave}
    >
      {content} card
    </div>
  );
};

const list = ["a", "b", "c", "d", "e", "f"];

function App() {
  const [arr, setArr] = useState(list.slice(0, 3));
  const [rest, setRest] = useState(list.slice(3));

  const [isScrolling, setIsScrolling] = useState(true);

  const updateArr = (idx?: number) => {
    const [a, b, c] = arr;

    if (idx === 0) {
      const lastRem = rest[rest.length - 1];
      const beforeArr = [lastRem, a, b];
      const beforeRem = [c, ...rest.slice(0, rest.length - 1)];
      setArr(beforeArr);
      setRest(beforeRem);
    } else {
      const firstRem = rest[0];
      const afterArr = [b, c, firstRem];
      const afterRem = [...rest.slice(1), a];
      setArr(afterArr);
      setRest(afterRem);
    }
  };

  useInterval(
    () => {
      updateArr();
    },
    isScrolling ? 3000 : null
  );
  return (
    <div>
      {arr.map((item, idx) => (
        <Card
          key={item}
          idx={idx}
          content={item}
          onClick={() => updateArr(idx)}
          onMouseEnter={() => setIsScrolling(false)}
          onMouseLeave={() => setIsScrolling(true)}
        />
      ))}
    </div>
  );
}

export default App;
```

`style.css`

```css
.card {
  position: absolute;
  width: 60%;
  height: 200px;
  left: 0;
  right: 0;
  margin: auto;
  transition: transform 0.4s ease;
  cursor: pointer;
  border: 1px solid silver;
  background-color: white;
  display: flex;
  align-items: center;
  padding: 8px;
  flex-direction: column;
  justify-content: center;
}
```

The final product and the code is available on [codesandbox](https://codesandbox.io/s/carousel-p1k933).

## Step by Step

Let's get started by setting up our initial carousel with three cards:

```tsx
import "./styles.css";

function App() {
  return (
    <div>
      <div className="card">Left card</div>
      <div className="card">Centre card</div>
      <div className="card">Right card</div>
    </div>
  );
}
```

![Just three divs](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/las1jnbocgj06dwzj1ix.png)


Next, we'll set the style. We're using `left`, `right`, and `margin` to centre the cards. This is achieved by using `position: absolute`, which takes the element out of the document flow. Unlike `position: fixed`, `position: absolute` is positioned relative to its closest ancestor.

```css
.card {
  position: absolute;
  width: 60%;
  height: 200px;
  left: 0;
  right: 0;
  margin: auto;
  background-color: white;
  border: 1px solid silver;
}
```

![Stacked cards](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/flyd0b2p9a714b36ow86.png)


At the moment, we can only see one card because the three cards are stacked on top of each other. To update the position and create a 3D effect, we'll shift two of the cards sideways using `transform` and `opacity`. The translate property is used with a value of ±40% to ensure that the cards don't extend beyond the screen since the width is 60% (along with the scaling). We're also reducing the size by 20% (`scale(0.8)`) to create the illusion of depth or distance. A `z-index` of 1 for the centre element ensures that it appears on top of the other two cards.

We are going to add inline styles to each of the `.card` elements
```tsx
import "./styles.css";

export default function App() {
  return (
    <div>
      <div
        className="card"
        // ↓ And this to shift it leftwards
        style={{ opacity: 0.4, transform: "translateX(-40%) scale(0.8)" }}
      >
        Left card
      </div>
      <div
        className="card"
        // ↓ And this to place it on top
        style={{ zIndex: 1 }}
      >
        Centre card
      </div>
      <div
        className="card"
        // ↓ And this to shift it rightwards
        style={{ opacity: 0.4, transform: "translateX(40%) scale(0.8)" }}
      >
        Right card
      </div>
    </div>
  );
}

```

![3d view enabled](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/us4u8vf5xy5v7sy79g4k.png)


Now, let's make it dynamic by adding functionality that allows us to move around the cards:


```tsx
// ↓ Import this
import { useState } from "react";
import "./styles.css";

// ↓ And a new function, `Card`
const Card = ({
  content,
  idx,
  onClick,
}: {
  content: string;
  idx: number;
  onClick: () => void;
}) => {
  let style = {};

  if (idx === 0)
    style = { opacity: 0.4, transform: "translateX(-40%) scale(0.8)" };
  if (idx === 1) style = { zIndex: 1 };
  if (idx === 2)
    style = { opacity: 0.4, transform: "translateX(40%) scale(0.8)" };

  return (
    <div className="card" style={style} onClick={onClick}>
      {content} card
    </div>
  );
};

export default function App() {
  // ↓ A dynamic list that can be updated
  const [arr, setArr] = useState(["Left", "Centre", "Right"]);

  // ↓ And a new function that will be used to rotate the arr
  const updateArr = (idx: number) => {
    const [a, b, c] = arr;
    // When you click the left card
    if (idx === 0) setArr([c, a, b]);
    // When you click the right or centre card
    else setArr([b, c, a]);
  };

  return (
    <div>
      {/* Replace the hardcoded DIVs */}
      {arr.map((item, idx) => (
        <Card
          key={item} // ← Note, do not use "idx" for key
          idx={idx}
          content={item}
          onClick={() => updateArr(idx)}
        />
      ))}
    </div>
  );
}
```

![Responsive cards to click](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5kz8a0rrvbcitssdfjuf.png)

If you click the cards, there will be a change in position but we need to add a rotation animation next. This is easy to achieve by adding a `transition` property to our CSS. The `transition` property works with any numbered properties, such as changing `opacity` from 0.4 to 1, translating from 40% to 0%, or scaling from 0.8 to 1. We'll add the `transition` property to the `.card` class in our CSS:

```css
.card {
  /* New code below */
  transition: 0.4s ease;
}
```


![Rotation animation enabled](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0fbaqbnzf9fltut3hni0.gif)


Now, let's automate it and create a continuous rotation:

```tsx
// ↓ Add useEffect and useRef to imports
import { useEffect, useRef, useState } from "react";
import "./styles.css";

// ↓ Create a new hook, `Card`
const useInterval = (
  callback: () => object | null | void,
  delay?: number | null
) => {
  const savedCallback = useRef<() => null | object | void>(() => null);

  useEffect(() => {
    savedCallback.current = callback;
  });

  useEffect(() => {
    if (delay !== null) {
      const interval = setInterval(() => savedCallback.current(), delay || 0);
      return () => clearInterval(interval);
    }

    return undefined;
  }, [delay]);
};

const Card = ({
  content,
  idx,
  onClick,
}: {
  content: string;
  idx: number;
  onClick: () => void;
}) => {
  let style = {};

  if (idx === 0)
    style = { opacity: 0.4, transform: "translateX(-40%) scale(0.8)" };
  if (idx === 1) style = { zIndex: 1 };
  if (idx === 2)
    style = { opacity: 0.4, transform: "translateX(40%) scale(0.8)" };

  return (
    <div className="card" style={style} onClick={onClick}>
      {content} card
    </div>
  );
};

export default function App() {
  const [arr, setArr] = useState(["Left", "Centre", "Right"]);

  const updateArr = (idx: number) => {
    const [a, b, c] = arr;
    // When you click the left card
    if (idx === 0) setArr([c, a, b]);
    // When you click the right or centre card
    else setArr([b, c, a]);
  };

  // ↓ Add the hook function
  useInterval(() => {
    updateArr(2);
    // rotate every 3 seconds
  }, 3000);

  return (
    <div>
      {arr.map((item, idx) => (
        <Card
          key={item}
          idx={idx}
          content={item}
          onClick={() => updateArr(idx)}
        />
      ))}
    </div>
  );
}
```

Finally, we can disable scrolling when the user hovers over the carousel:

```tsx
import { useEffect, useRef, useState } from "react";
import "./styles.css";

const useInterval = (
  callback: () => object | null | void,
  delay?: number | null
) => {
  const savedCallback = useRef<() => null | object | void>(() => null);

  useEffect(() => {
    savedCallback.current = callback;
  });

  useEffect(() => {
    if (delay !== null) {
      const interval = setInterval(() => savedCallback.current(), delay || 0);
      return () => clearInterval(interval);
    }

    return undefined;
  }, [delay]);
};

const Card = ({
  content,
  idx,
  onClick,
  onMouseEnter, // ← Add this
  onMouseLeave, // ← Add this
}: {
  content: string;
  idx: number;
  onClick: () => void;
  onMouseEnter: () => void; // ← Add this: indicates hovering started
  onMouseLeave: () => void; // ← Add this: indicates hovering started
}) => {
  let style = {};

  if (idx === 0)
    style = {
      opacity: 0.4,
      transform: "translateX(-40%) scale(0.8)",
      zIndex: 0,
    };
  if (idx === 1) style = { zIndex: 1 };
  if (idx === 2)
    style = {
      opacity: 0.4,
      transform: "translateX(40%) scale(0.8)",
      zIndex: 0,
    };

  return (
    <div
      className="card"
      style={style}
      onClick={onClick}
      // ↓ Attach it to the div
      onMouseEnter={onMouseEnter} // ← Add this
      onMouseLeave={onMouseLeave} // ← Add this
    >
      {content} card
    </div>
  );
};

export default function App() {
  const [arr, setArr] = useState(["Left", "Centre", "Right"]);
  // ↓ Will be used to toggle auto scrolling
  const [isRolling, setIsRolling] = useState(true);

  const updateArr = (idx: number) => {
    const [a, b, c] = arr;
    if (idx === 0) setArr([c, a, b]);
    else setArr([b, c, a]);
  };

  useInterval(
    () => {
      updateArr(2);
    },
    // // 3000
    isRolling ? 3000 : null // ← Add logic to determine scrolling
  );

  return (
    <div>
      {arr.map((item, idx) => (
        <Card
          key={item}
          idx={idx}
          content={item}
          onClick={() => updateArr(idx)}
          // ↓ Update the props
          onMouseEnter={() => setIsRolling(false)}
          onMouseLeave={() => setIsRolling(true)}
        />
      ))}
    </div>
  );
}
```

If you need to handle more than three items, you can achieve this by adding to the array while ensuring that it doesn't go beyond three. One solution is to have two lists: an active one and a remainder list:

```tsx
// ↓ Add a list with more than 3 items
const list = ["a", "b", "c", "d", "e", "f"];

export default function App() {
  // Two list, one is limited to 3 items
  const [arr, setArr] = useState(list.slice(0, 3));
  // Will contain any amount of items, and may be updated dynamically
  const [rest, setRest] = useState(list.slice(3));

  const [isRolling, setIsRolling] = useState(true);

  // ↓ re-write function to update two states at the same time (arr, and rest)
  const updateArr = (idx?: number) => {
    const [a, b, c] = arr;

    if (idx === 0) {
      // It will basically dispose of the last item in both arrays, and insert
      // it in front of the other array
      const lastRestItem = rest[rest.length - 1];
      const newArr = [lastRestItem, a, b];
      const newRest = [c, ...rest.slice(0, rest.length - 1)];
      setArr(newArr);
      setRest(newRest);
    } else {
      // It will basically dispose of the first item in both arrays, and add it
      // to the end of the other array.
      const firstRestItem = rest[0];
      const newArr = [b, c, firstRestItem];
      const newRest = [...rest.slice(1), a];
      setArr(newArr);
      setRest(newRest);
    }
  };

  useInterval(
    () => {
      updateArr();
    },
    isRolling ? 3000 : null
  );
  return (
    <div>
      {arr.map((item, idx) => (
        <Card
          key={item}
          idx={idx}
          content={item}
          onClick={() => updateArr(idx)}
          onMouseEnter={() => setIsRolling(false)}
          onMouseLeave={() => setIsRolling(true)}
        />
      ))}
    </div>
  );
}
```
In case you are wondering what is going on inside `updateArr` function, we are basically shifting the items left or right

![Shift right](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f7qg9vujogvwi8eb8fbz.png)

![Shift left](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/aln5g4oqacwcuyso0w7w.png)



That's it! By following this step-by-step guide, you should now have a fully functional and customizable carousel that can be easily integrated into your React-based projects. 

Thanks for reading, and if you have any suggestions or feedback, feel free to share them in the comments below!