This article was originally published at https://whatisweb.dev/building-a-responsive-carousel-component-in-react-the-complete-guide
In web design, a carousel component is used to display multiple images or pieces of content in a rotating or sliding manner. The carousel component is typically used to highlight featured products, showcase portfolios, present testimonials, or display news articles or blog posts.
While there are awesome libraries and components like Swiper that are used to create responsive and accessible carousels, it's still important to know how to create these components from scratch.
Creating these components from scratch will make you a better React developer and help you appreciate the work that goes into creating these libraries and components.
So without further ado, let’s get started. :)
Getting Started
Before starting, you must have npm
installed on your computer, which comes bundled with Node.js which you can install from here.
Project Setup
Run the following commands to initialize the React application and install the required dependencies.
npm create vite@latest react-carousel --template react
cd react-carousel
npm install
npm run dev
Folder Structure
Before we start writing any code, let's create the folder structure required for the front-end application.
You can create the above directories with these commands.
cd react-carousel/src
mkdir components hooks
(Optional) Adding fonts and styles
- Add these fonts to the
head
section of theindex.html
file under the project’s root directory.
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
- Replace the
CSS
inside theindex.css
with the code below:
*::before,
*,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.material-symbols-outlined {
font-variation-settings: "FILL" 0, "wght" 600, "GRAD" 0, "opsz" 48;
}
body {
margin: 0;
font-family: "Poppins", sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: #9EC6F3;
}
Basic Carousel Layout
Now that we have created the initial set-up required to build and run the application, let’s start by creating the basic carousel layout — that is, the barebones HTML structure of the carousel component.
Note:— We will use CSS modules to style our React application. So for each component, we will also create a CSS or SCSS file for that component.
Create the Slider.jsx
and Slider.module.css
files inside the components/Slider
folder and add the following code to them.
Slider.jsx
//Slider.jsx
import { Children } from "react";
import styles from "./Slider.module.css";
const Slider = ({
items,
children,
}) => {
const sliderItems = items || Children.toArray(children);
const sliderButtonHandler = (direction) => {
if (direction === "forward") {
// TODO: Add logic to slide to next item
} else if (direction === "backward") {
// TODO: Add logic to slide to previous item
}
};
return (
<div className={styles.slider}>
<div className={styles.slidesContainer}>
<div style={{ display: "flex" }}>
{sliderItems.map((item, index) => {
return (
<div key={index}>
{item}
</div>
);
})}
</div>
</div>
<button
className={`${styles.slideButton} ${styles.slideButtonPrev}`}
onClick={() => sliderButtonHandler("backward")}
>
<span
className={`material-symbols-outlined ${styles.slideButtonIcon}`}
style={{ letterSpacing: "4px" }}
>
arrow_back_ios_new
</span>
</button>
<button
className={`${styles.slideButton} ${styles.slideButtonNext}`}
onClick={() => sliderButtonHandler("forward")}
>
<span className={`material-symbols-outlined ${styles.slideButtonIcon}`}>
arrow_forward_ios
</span>
</button>
</div>
);
};
export default Slider;
Slider.module.css
.slider {
position: relative;
max-width: 1300px;
width: 100%;
}
.slidesContainer {
overflow: hidden;
width: 100%;
margin-left: 8px;
}
.slideButton {
position: absolute;
top: 50%;
transform: translateY(-50%);
height: 48px;
width: 48px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
box-shadow: rgb(0 0 0 / 15%) 0px 4px 10px;
border: none;
cursor: pointer;
}
.slideButton:hover {
box-shadow: rgb(0 0 0 / 25%) 0px 4px 10px;
}
.slideButtonPrev {
left: -24px;
}
.slideButtonNext {
right: -24px;
}
.slideButtonIcon {
color: #353840;
font-variation-settings: "FILL" 0, "wght" 700, "GRAD" 0, "opsz" 48;
}
@media screen and (max-width: 768px) {
.slideButtonPrev {
left: 0;
}
.slideButtonNext {
right: 0;
}
}
Modify the App.jsx
file to include the newly created Slider
component.
App.jsx
import Slider from './components/Slider/Slider';
import './App.css';
function App() {
const sliderItems = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
return (
<div>
<Slider>
{sliderItems.map((item, index) => (
<div className="item" key={index}>{item}</div>
))}
</Slider>
</div>
);
}
export default App;
App.css
.item {
height: 310px;
border-radius: 16px;
background: #FFF1D5;
display: flex;
align-items: center;
justify-content: center;
color: #604652;
font-size: 102px;
}
If you open the application in your browser, you’ll notice that the component we created doesn’t look anything like a carousel yet. But bear with me for a moment—we’ll soon add the necessary styles and functionality to turn it into a fully working carousel.
Before we dive into that, let me briefly explain the code we’ve written so far.
The Slider
component accepts two props — items
and children
— either of which can be used to render the carousel content. We’ve also added two navigation buttons to move the slider forward and backward. Finally, we applied basic CSS styling to the component.
In the App
component, we pass data to the Slider
component by mapping over the sliderItems
array.
Carousel Logic
Now that we have the layout of the carousel ready, let’s add the functionality needed to turn it into a working carousel.
Update the code inside the Slider.jsx
and App.jsx
with the code below.
Slider.jsx
import { Children, useState, useRef, useEffect } from "react";
import styles from "./Slider.module.css";
const Slider = ({
slidesPerView: initialSliderPerView = 4,
spaceBetween: initialSpaceBetween = 16,
slidesPerGroup: initialSliderPerGroup = 4,
items,
children,
}) => {
// New Code
const [slidesPerView, setSlidesPerView] = useState(initialSliderPerView);
const [slidesPerGroup, setSlidesPerGroup] = useState(initialSliderPerGroup);
const [spaceBetween, setSpaceBetween] = useState(initialSpaceBetween);
const [sliderItemWidth, setSliderItemWidth] = useState(0);
const sliderContainerRef = useRef(null);
// New Code
const sliderItems = items || Children.toArray(children);
// New Code
useEffect(() => {
if (sliderContainerRef.current) {
const sliderContainerWidth = sliderContainerRef.current.offsetWidth;
const elements = sliderContainerRef.current.querySelectorAll('.slider-item');
elements.forEach(el => {
const sliderItemWidth = Math.ceil((sliderContainerWidth / slidesPerView) - spaceBetween);
el.style.width = sliderItemWidth + 'px';
Array.from(el.children).forEach(div => {
div.style.width = sliderItemWidth + 'px';
});
setSliderItemWidth(sliderItemWidth);
});
}
}, [slidesPerView, spaceBetween]);
// New Code
const sliderButtonHandler = (direction) => {
if (direction === "forward") {
// TODO: Add logic to slide to next item
} else if (direction === "backward") {
// TODO: Add logic to slide to previous item
}
};
return (
<div className={styles.slider}>
<div className={styles.slidesContainer} ref={sliderContainerRef}>
<div style={{ display: "flex" }}>
{sliderItems.map((item, index) => {
return (
<div
className="slider-item"
key={index}
style={{
marginRight: Number(spaceBetween)
? `${spaceBetween}px`
: "0px",
}}
>
{item}
</div>
);
})}
</div>
</div>
<button
className={`${styles.slideButton} ${styles.slideButtonPrev}`}
onClick={() => sliderButtonHandler("backward")}
>
<span
className={`material-symbols-outlined ${styles.slideButtonIcon}`}
style={{ letterSpacing: "4px" }}
>
arrow_back_ios_new
</span>
</button>
<button
className={`${styles.slideButton} ${styles.slideButtonNext}`}
onClick={() => sliderButtonHandler("forward")}
>
<span className={`material-symbols-outlined ${styles.slideButtonIcon}`}>
arrow_forward_ios
</span>
</button>
</div>
);
};
export default Slider;
App.jsx
import Slider from './components/Slider/Slider';
import './App.css';
function App() {
const sliderItems = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
return (
<div className="flex-container">
<Slider
slidesPerView={4}
slidesPerGroup={4}
spaceBetween={16}
>
{sliderItems.map((item, index) => (
<div className="item" key={index}>{item}</div>
))}
</Slider>
</div>
);
}
export default App;
App.css
.flex-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.item {
height: 310px;
border-radius: 16px;
background: #FFF1D5;
display: flex;
align-items: center;
justify-content: center;
color: #604652;
font-size: 102px;
}
If you open the application in your browser, you’ll see that the component is starting to look like a carousel, but it doesn’t function like one yet.
Before we make it fully interactive, let’s quickly review the code we’ve written so far.
The Slider
component now accepts three additional props: slidesPerView
, slidesPerGroup
, and spaceBetween
.
slidesPerView
controls how many items are visible in the viewport at once.slidesPerGroup
defines how many items move with each navigation.spaceBetween
sets the spacing between each slide.
The component also uses a sliderItemWidth
state variable to store the calculated width of each slide. This value is computed using the useEffect
hook and a sliderContainerRef
, which allow us to dynamically measure the container's width and divide it based on the number of slides per view.
Making it Functional
Let’s finally add the logic to turn it into a fully working carousel.
Update the Slider.jsx
with the following code:
Slider.jsx
// Slider.jsx
import { Children, useState, useRef, useEffect } from "react";
import useInView from "../../hooks/useInView";
import styles from "./Slider.module.css";
const Slider = ({
slidesPerView: initialSliderPerView = 4,
spaceBetween: initialSpaceBetween = 16,
slidesPerGroup: initialSliderPerGroup = 4,
items,
children,
}) => {
const [slidesPerView, setSlidesPerView] = useState(initialSliderPerView);
const [slidesPerGroup, setSlidesPerGroup] = useState(initialSliderPerGroup);
const [spaceBetween, setSpaceBetween] = useState(initialSpaceBetween);
// New Code
const [activeSlideIndex, setActiveSlideIndex] = useState(0);
// New Code
const [sliderItemWidth, setSliderItemWidth] = useState(0);
const sliderContainerRef = useRef(null);
// New Code
const { inView: lastSliderItemInView, ref: lastSliderItemRef } = useInView({
root: sliderContainerRef.current,
threshold: 0.95,
});
const { inView: firstSliderItemInView, ref: firstSliderItemRef } = useInView({
root: sliderContainerRef.current,
threshold: 1.0,
});
// New Code
const sliderItems = items || Children.toArray(children);
useEffect(() => {
if (sliderContainerRef.current) {
const sliderContainerWidth = sliderContainerRef.current.offsetWidth;
const elements = sliderContainerRef.current.querySelectorAll('.slider-item');
elements.forEach(el => {
const sliderItemWidth = Math.ceil((sliderContainerWidth / slidesPerView) - spaceBetween);
el.style.width = sliderItemWidth + 'px';
Array.from(el.children).forEach(elem => {
elem.style.width = sliderItemWidth + 'px';
});
setSliderItemWidth(sliderItemWidth);
});
}
}, [slidesPerView, spaceBetween]);
// New Code
const sliderButtonHandler = (direction) => {
if (direction === "forward") {
if (!lastSliderItemInView && activeSlideIndex < (sliderItems.length - slidesPerGroup)) {
setActiveSlideIndex((prevIndex) => {
if((sliderItems.length - prevIndex) < slidesPerGroup && sliderItems.length - prevIndex != 0) {
return sliderItems.length;
}
return prevIndex + slidesPerGroup;
});
}
} else if (direction === "backward") {
if (!firstSliderItemInView && activeSlideIndex > 0) {
setActiveSlideIndex((prevIndex) => {
if(prevIndex < slidesPerGroup) {
return 0;
}
return prevIndex - slidesPerGroup;
});
}
}
};
const setSliderItemRef = (index, sliderItemsArray) => {
if (index === 0) {
return firstSliderItemRef;
}
if (index === sliderItemsArray.length - 1) {
return lastSliderItemRef;
}
return null;
};
// New Code
return (
<div className={styles.slider}>
<div className={styles.slidesContainer} ref={sliderContainerRef}>
<div style={{
display: "flex",
transition: "all 0.5s ease-in-out",
transform: `translateX(${
(sliderItemWidth + spaceBetween) * activeSlideIndex * -1
}px)`,
}}
>
{sliderItems.map((item, index, array) => {
return (
<div
className="slider-item"
key={index}
ref={setSliderItemRef(index, array)}
style={{
marginRight: Number(spaceBetween)
? `${spaceBetween}px`
: "0px",
}}
>
{item}
</div>
);
})}
</div>
</div>
<button
className={`${styles.slideButton} ${styles.slideButtonPrev}`}
onClick={() => sliderButtonHandler("backward")}
>
<span
className={`material-symbols-outlined ${styles.slideButtonIcon}`}
style={{ letterSpacing: "4px" }}
>
arrow_back_ios_new
</span>
</button>
<button
className={`${styles.slideButton} ${styles.slideButtonNext}`}
onClick={() => sliderButtonHandler("forward")}
>
<span className={`material-symbols-outlined ${styles.slideButtonIcon}`}>
arrow_forward_ios
</span>
</button>
</div>
);
};
export default Slider;
The updated code uses a custom useInView
hook, which checks whether a slide is currently visible in the viewport.
Create a new file named useInView.js
inside the hooks
directory, and add the following code to it:
useInView.js
// useInView.js
import { useCallback, useEffect, useRef, useState } from "react";
const useInView = (
options = {
root: null,
rootMargin: "0px",
threshold: 1.0,
}
) => {
const [inView, setInView] = useState(false);
const targetRef = useRef(null);
const inViewRef = useCallback((node) => {
if (node) {
targetRef.current = node;
}
}, []);
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setInView(true);
} else {
setInView(false);
}
});
}, options);
if (targetRef.current) {
observer.observe(targetRef.current);
}
}, [options]);
return { ref: targetRef, inView, inViewRef };
};
export default useInView;
We’ve made quite a few changes to the code to make the carousel interactive. Let’s quickly review the changes one by one:
We introduced a new state variable,
activeSlideIndex
, which keeps track of the index of the leftmost visible slide in the viewport.We added a custom hook,
useInView
, that detects when a slide enters the viewport. This hook helps determine when the first or last slide becomes visible.The
sliderButtonHandler
function has been updated to handle carousel navigation. It adjusts theactiveSlideIndex
based on whether the user clicks the forward or backward button.Finally, we applied CSS
transform
to the slider container using thetranslateX
function, which uses theactiveSlideIndex
to move the carousel forward or backward.
AutoPlay & Loop
Now that we’ve implemented basic navigation to move the carousel forward and backward using the navigation buttons, let’s make it more dynamic by adding autoplay and looping functionality.
These features will allow the carousel to automatically move to the next slide at regular intervals and loop back to the first slide once it reaches the end.
Update the Slider.jsx
with the following code:
// Slider.jsx
import { Children, useState, useRef, useEffect } from "react";
import styles from "./Slider.module.css";
import useInView from "../../hooks/useInView";
const Slider = ({
slidesPerView: initialSliderPerView = 4,
spaceBetween: initialSpaceBetween = 16,
slidesPerGroup: initialSliderPerGroup = 4,
items,
children,
loop,
autoPlay = false,
autoPlayInterval = 2000,
}) => {
const [slidesPerView, setSlidesPerView] = useState(initialSliderPerView);
const [slidesPerGroup, setSlidesPerGroup] = useState(initialSliderPerGroup);
const [spaceBetween, setSpaceBetween] = useState(initialSpaceBetween);
// New Code
const [activeSlideIndex, setActiveSlideIndex] = useState(
loop ? slidesPerView : 0
);
const [transitionEnabled, setTransitionEnabled] = useState(false);
// New Code
const [sliderItemWidth, setSliderItemWidth] = useState(0);
const sliderContainerRef = useRef(null);
const { inView: lastSliderItemInView, ref: lastSliderItemRef } = useInView({
root: sliderContainerRef.current,
threshold: 0.95,
});
const { inView: firstSliderItemInView, ref: firstSliderItemRef } = useInView({
root: sliderContainerRef.current,
threshold: 1.0,
});
items = items || Children.toArray(children);
// New Code
const sliderItems = loop
? [
...items.slice(-slidesPerView),
...items,
...items.slice(0, slidesPerView),
]
: items;
// New Code
useEffect(() => {
if (sliderContainerRef.current) {
const sliderContainerWidth = sliderContainerRef.current.offsetWidth;
const elements = sliderContainerRef.current.querySelectorAll('.slider-item');
elements.forEach(el => {
const sliderItemWidth = Math.ceil((sliderContainerWidth / slidesPerView) - spaceBetween);
el.style.width = sliderItemWidth + 'px';
Array.from(el.children).forEach(div => {
div.style.width = sliderItemWidth + 'px';
});
setSliderItemWidth(sliderItemWidth);
});
}
}, [slidesPerView, spaceBetween]);
// New Code
useEffect(() => {
setTimeout(() => {
setTransitionEnabled(true);
}, 100);
}, [firstSliderItemRef]);
useEffect(() => {
let intervalID;
if (loop && autoPlay) {
intervalID = setInterval(() => {
if (
activeSlideIndex === slidesPerGroup ||
activeSlideIndex === items.length
) {
setTransitionEnabled(true);
}
setActiveSlideIndex((prevIndex) => prevIndex + slidesPerGroup);
}, autoPlayInterval);
}
return () => {
if (intervalID) {
clearInterval(intervalID);
}
};
}, [
loop,
slidesPerGroup,
activeSlideIndex,
items.length,
autoPlay,
autoPlayInterval,
]);
// New Code
// New Code
const sliderButtonHandler = (direction) => {
if (
activeSlideIndex === slidesPerGroup ||
activeSlideIndex === items.length
) {
setTransitionEnabled(true);
}
if (direction === "forward") {
if (loop || (!loop && !lastSliderItemInView && activeSlideIndex < (items.length - slidesPerGroup))) {
setActiveSlideIndex((prevIndex) => {
if((items.length - prevIndex) < slidesPerGroup && items.length - prevIndex != 0) {
return items.length;
}
return prevIndex + slidesPerGroup;
});
}
} else if (direction === "backward") {
if (loop || (!loop && !firstSliderItemInView && activeSlideIndex > 0)) {
setActiveSlideIndex((prevIndex) => {
if(prevIndex < slidesPerGroup) {
return 0;
}
return prevIndex - slidesPerGroup;
});
}
}
};
const handleTransitionEnd = () => {
if (loop) {
if (activeSlideIndex > items.length) {
setTransitionEnabled(false);
setActiveSlideIndex(slidesPerGroup);
} else if (activeSlideIndex === 0) {
setTransitionEnabled(false);
setActiveSlideIndex(items.length);
}
}
};
// New Code
const setSliderItemRef = (index, sliderItemsArray) => {
if (loop && index === 0) {
return firstSliderItemRef;
}
if (!loop) {
if (index === 0) {
return firstSliderItemRef;
}
if (index === sliderItemsArray.length - 1) {
return lastSliderItemRef;
}
}
return null;
};
return (
<div className={styles.slider}>
<div className={styles.slidesContainer} ref={sliderContainerRef}>
<div
onTransitionEnd={handleTransitionEnd}
style={{
display: "flex",
transition: !transitionEnabled ? "none" : "all 0.5s ease-in-out",
transform: `translateX(${
(sliderItemWidth + spaceBetween) * activeSlideIndex * -1
}px)`,
}}
>
{sliderItems.map((item, index, array) => {
return (
<div
className="slider-item"
key={index}
ref={setSliderItemRef(index, array)}
style={{
marginRight: Number(spaceBetween)
? `${spaceBetween}px`
: "0px",
}}
>
{item}
</div>
);
})}
</div>
</div>
<button
className={`${styles.slideButton} ${styles.slideButtonPrev}`}
onClick={() => sliderButtonHandler("backward")}
>
<span
className={`material-symbols-outlined ${styles.slideButtonIcon}`}
style={{ letterSpacing: "4px" }}
>
arrow_back_ios_new
</span>
</button>
<button
className={`${styles.slideButton} ${styles.slideButtonNext}`}
onClick={() => sliderButtonHandler("forward")}
>
<span className={`material-symbols-outlined ${styles.slideButtonIcon}`}>
arrow_forward_ios
</span>
</button>
</div>
);
};
export default Slider;
We have made a lot of changes to the component, let’s go through them one by one:
The slider component now accepts three more props —
loop
,autoPlay
andautoPlayInterval
.-
If looping is enabled,
sliderItems
becomes:
const sliderItems = [ ...items.slice(-slidesPerView), ...items, ...items.slice(0, slidesPerView), ];
This allows the slider to simulate an infinite loop. When the user scrolls to the "end" or "start" of the carousel, the fake extra items allow a seamless visual transition back to the start or end, and the slider can be quickly repositioned behind the scenes.
The initial value of
activeSlideIndex
is set to the value ofslidesPerView
when looping is enabled.The
sliderButtonHandler
function has been updated to handle looping behavior, adjusting theactiveSlideIndex
appropriately when navigating forward or backwardA new
useEffect
hook has been introduced to implement the autoplay functionality. It automatically updates theactiveSlideIndex
at a specified interval, moving the carousel forward without user interaction.A
handleTransitionEnd
function has been added to temporarily remove the CSS transition from the slider container once the carousel reaches the end.Another
useEffect
hook has been introduced that re-enables the transition when the first item becomes visible again, ensuring a smooth loop back to the beginning.
Finally update the App.jsx
to enable autoplay and looping:
import Slider from './components/Slider/Slider';
import './App.css';
function App() {
const sliderItems = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
return (
<div className="flex-container">
<Slider
slidesPerView={4}
slidesPerGroup={4}
spaceBetween={16}
autoPlay={true}
autoPlayInterval={2000}
loop={true}
>
{sliderItems.map((item, index) => (
<div className="item" key={index}>{item}</div>
))}
</Slider>
</div>
);
}
export default App;
Responsive Design
That was a lot of code we wrote, but there’s still one important piece of functionality that’s still missing — making the carousel responsive. Let’s finally add the code required to make it responsive across devices of different resolutions.
Update the Slider.jsx
with the following code.
import { useRef, useState, useEffect, Children } from "react";
import useInView from "../../hooks/useInView";
import useWindowDimensions from "../../hooks/useWindowDimensions";
import styles from "./Slider.module.css";
const Slider = ({
slidesPerView: initialSliderPerView,
spaceBetween: initialSpaceBetween,
slidesPerGroup: initialSliderPerGroup,
loop,
breakpoints,
items,
children,
autoPlay = false,
autoPlayInterval = 2000,
}) => {
const [slidesPerView, setSlidesPerView] = useState(initialSliderPerView);
const [slidesPerGroup, setSlidesPerGroup] = useState(initialSliderPerGroup);
const [spaceBetween, setSpaceBetween] = useState(initialSpaceBetween);
const [activeSlideIndex, setActiveSlideIndex] = useState(
loop ? slidesPerView : 0
);
const [transitionEnabled, setTransitionEnabled] = useState(false);
const [sliderItemWidth, setSliderItemWidth] = useState(0);
const sliderContainerRef = useRef(null);
// New Code
const { width: deviceWidth } = useWindowDimensions();
// New Code
const { inView: lastSliderItemInView, ref: lastSliderItemRef } = useInView({
root: sliderContainerRef.current,
threshold: 0.95,
});
const { inView: firstSliderItemInView, ref: firstSliderItemRef } = useInView({
root: sliderContainerRef.current,
threshold: 1.0,
});
items = items || Children.toArray(children);
useEffect(() => {
if (sliderContainerRef.current) {
const sliderContainerWidth = sliderContainerRef.current.offsetWidth;
const elements = sliderContainerRef.current.querySelectorAll('.slider-item');
elements.forEach(el => {
const sliderItemWidth = Math.ceil((sliderContainerWidth / slidesPerView) - spaceBetween);
el.style.width = sliderItemWidth + 'px';
Array.from(el.children).forEach(div => {
div.style.width = sliderItemWidth + 'px';
});
setSliderItemWidth(sliderItemWidth);
});
}
}, [slidesPerView, spaceBetween]);
useEffect(() => {
setTimeout(() => {
setTransitionEnabled(true);
}, 100);
}, [firstSliderItemRef]);
// New Code
useEffect(() => {
if (breakpoints) {
Object.keys(breakpoints).forEach((breakpoint) => {
if (Number(breakpoint) && deviceWidth >= Number(breakpoint)) {
setSlidesPerView(
(prev) => breakpoints[breakpoint].slidesPerView || prev
);
setSlidesPerGroup(
(prev) => breakpoints[breakpoint].slidesPerGroup || prev
);
setSpaceBetween(
(prev) => breakpoints[breakpoint].spaceBetween || prev
);
if(loop) {
setActiveSlideIndex((prev) => breakpoints[breakpoint].slidesPerView || prev);
}
}
});
}
}, [deviceWidth, breakpoints, loop]);
// New Code
useEffect(() => {
let intervalID;
if (loop && autoPlay) {
intervalID = setInterval(() => {
if (
activeSlideIndex === slidesPerGroup ||
activeSlideIndex === items.length
) {
setTransitionEnabled(true);
}
setActiveSlideIndex((prevIndex) => prevIndex + slidesPerGroup);
}, autoPlayInterval);
}
return () => {
if (intervalID) {
clearInterval(intervalID);
}
};
}, [
loop,
slidesPerGroup,
activeSlideIndex,
items.length,
autoPlay,
autoPlayInterval,
]);
const sliderButtonHandler = (direction) => {
if (
activeSlideIndex === slidesPerGroup ||
activeSlideIndex === items.length
) {
setTransitionEnabled(true);
}
if (direction === "forward") {
if (loop || (!loop && !lastSliderItemInView && activeSlideIndex < (items.length - slidesPerGroup))) {
setActiveSlideIndex((prevIndex) => {
if((items.length - prevIndex) < slidesPerGroup && items.length - prevIndex != 0) {
return items.length;
}
return prevIndex + slidesPerGroup;
});
}
} else if (direction === "backward") {
if (loop || (!loop && !firstSliderItemInView && activeSlideIndex > 0)) {
setActiveSlideIndex((prevIndex) => {
if(prevIndex < slidesPerGroup) {
return 0;
}
return prevIndex - slidesPerGroup;
});
}
}
};
const handleTransitionEnd = () => {
if (loop) {
if (activeSlideIndex > items.length) {
setTransitionEnabled(false);
setActiveSlideIndex(slidesPerGroup);
} else if (activeSlideIndex === 0) {
setTransitionEnabled(false);
setActiveSlideIndex(items.length);
}
}
};
const sliderItems = loop
? [
...items.slice(-slidesPerView),
...items,
...items.slice(0, slidesPerView),
]
: items;
const setSliderItemRef = (index, sliderItemsArray) => {
if (loop && index === 0) {
return firstSliderItemRef;
}
if (!loop) {
if (index === 0) {
return firstSliderItemRef;
}
if (index === sliderItemsArray.length - 1) {
return lastSliderItemRef;
}
}
return null;
};
return (
<div className={styles.slider}>
<div className={styles.slidesContainer} ref={sliderContainerRef}>
<div
onTransitionEnd={handleTransitionEnd}
style={{
display: "flex",
transition: !transitionEnabled ? "none" : "all 0.5s ease-in-out",
transform: `translateX(${
(sliderItemWidth + spaceBetween) * activeSlideIndex * -1
}px)`,
marginBottom: "3px",
}}
>
{sliderItems.map((item, index, array) => {
return (
<div
className="slider-item"
key={index}
ref={setSliderItemRef(index, array)}
style={{
marginRight: Number(spaceBetween)
? `${spaceBetween}px`
: "0px",
}}
>
{item}
</div>
);
})}
</div>
</div>
<button
className={`${styles.slideButton} ${styles.slideButtonPrev}`}
onClick={() => sliderButtonHandler("backward")}
>
<span
className={`material-symbols-outlined ${styles.slideButtonIcon}`}
style={{ letterSpacing: "4px" }}
>
arrow_back_ios_new
</span>
</button>
<button
className={`${styles.slideButton} ${styles.slideButtonNext}`}
onClick={() => sliderButtonHandler("forward")}
>
<span className={`material-symbols-outlined ${styles.slideButtonIcon}`}>
arrow_forward_ios
</span>
</button>
</div>
);
};
export default Slider;
Create a new file named useWindowDimensions.js
inside the hooks
directory and add the following code to it:
useWindowDimensions.js
import { useState, useEffect } from "react";
function getWindowDimensions() {
const { innerWidth: width, innerHeight: height } = window;
return {
width,
height,
};
}
export default function useWindowDimensions() {
const [windowDimensions, setWindowDimensions] = useState(
getWindowDimensions()
);
useEffect(() => {
function handleResize() {
setWindowDimensions(getWindowDimensions());
}
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
return windowDimensions;
}
Let’s quickly go through the changes we made one by one:
The updated code uses another custom
useWindowDimensions
hook, which returns the height and width of the browser window both when it is first initialized and whenever the window is resized.The component now accepts one additional prop —
breakpoints
, an object that defines how many items should be visible in the carousel based on the device’s screen width.Each key in the
breakpoints
object (e.g.,320
,768
,1366
) represents a minimum viewport width (in pixels). The corresponding value defines the layout configuration at that width.A new
useEffect
hook has been added to dynamically update theslidesPerView
,slidesPerGroup
, andspaceBetween
values according to the current screen width, if thebreakpoints
prop is provided
Finally update the App.jsx
to enable responsive behavior:
import Slider from './components/Slider/Slider';
import './App.css';
function App() {
const sliderItems = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
return (
<div className="flex-container">
<Slider
spaceBetween={16}
slidesPerView={4}
slidesPerGroup={4}
loop={true}
autoPlay={true}
autoPlayInterval={4000}
breakpoints={{
320: {
slidesPerView: 1,
slidesPerGroup: 1,
},
1366: {
slidesPerView: 4,
slidesPerGroup: 4,
spaceBetween: 18
},
}}
>
{sliderItems.map((item, index) => (
<div className="item" key={index}>{item}</div>
))}
</Slider>
</div>
);
}
export default App;
Final Result
Source Code
You can download the source code from my GitHub repo:
SukhjinderArora
/
react-carousel
A responsive React carousel component
Building a Responsive Carousel Component in React: The Complete Guide
Learn How to Create a Responsive Carousel Component in React
Run the following commands to run this application locally on your system:
npm install
npm run dev
You can read the full article at: https://whatisweb.dev/building-a-responsive-carousel-component-in-react-the-complete-guide
Conclusion
In this article, we walked through building a fully functional, customizable, and responsive carousel component in React from scratch. Starting with a basic layout, we incrementally added key features like navigation controls, autoplay and loop.
Finally, we implemented responsive behavior using the breakpoints
prop, allowing the carousel to adapt seamlessly to different screen sizes and device resolutions.
By now, you should have a solid understanding of how to build a flexible carousel component that can be reused and extended in your own projects.
That’s it and hope you found this article helpful! Please feel free to comment below and ask anything, suggest feedback or just chat. You can also follow me on Hashnode, Medium and Twitter. Cheers! ⭐️
Top comments (0)