DEV Community

Cover image for How to create a carousel in React
Ade Adeola
Ade Adeola

Posted on • Updated on

 

How to create a carousel in React

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

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;
Enter fullscreen mode Exit fullscreen mode

style.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;
}
Enter fullscreen mode Exit fullscreen mode

The final product and the code is available on codesandbox.

Step by Step

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

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Just three divs

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.

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

Stacked cards

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

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>
  );
}

Enter fullscreen mode Exit fullscreen mode

3d view enabled

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

// ↓ 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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Responsive cards to click

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:

.card {
  /* New code below */
  transition: 0.4s ease;
}
Enter fullscreen mode Exit fullscreen mode

Rotation animation enabled

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

// ↓ 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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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:

// ↓ 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>
  );
}
Enter fullscreen mode Exit fullscreen mode

In case you are wondering what is going on inside updateArr function, we are basically shifting the items left or right

Shift right

Shift left

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!

Top comments (0)