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;
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;
}
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>
);
}
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;
}
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>
);
}
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>
);
}
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;
}
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>
);
}
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>
);
}
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>
);
}
In case you are wondering what is going on inside updateArr
function, we are basically shifting the items left or right
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)