Building a Responsive Carousel Component in React
Learn how to build a fully functional, responsive carousel component from scratch in React with autoplay, looping, and responsive design features.
Building a Responsive Carousel Component in React
A carousel component is a crucial UI element used to display multiple images or pieces of content in a rotating or sliding manner. It's commonly used to highlight featured products, showcase portfolios, present testimonials, or display news articles and blog posts.
While there are excellent libraries like Swiper that provide ready-to-use carousel components, understanding how to build one from scratch will make you a better React developer and help you appreciate the complexity behind these libraries.
Table of Contents
- Getting Started
- Project Setup
- Basic Carousel Layout
- Carousel Logic
- Making it Functional
- AutoPlay & Loop
- Responsive Design
- Complete Implementation
Getting Started
Before we begin, ensure you have Node.js and npm installed on your computer. You can download Node.js from nodejs.org.
Project Setup
Let's start by creating a new React project using Vite:
npm create vite@latest react-carousel --template react
cd react-carousel
npm install
npm run devFolder Structure
Create the following folder structure for your project:
react-carousel/
├── src/
│ ├── components/
│ │ └── Slider/
│ │ ├── Slider.jsx
│ │ └── Slider.module.css
│ ├── hooks/
│ │ ├── useInView.js
│ │ └── useWindowDimensions.js
│ ├── App.jsx
│ ├── App.css
│ └── index.cssYou can create these directories with:
cd react-carousel/src
mkdir components hooks
cd components
mkdir SliderAdding Fonts and Styles
Add these fonts to the <head> section of your index.html:
<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 in index.css with:
*::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
Let's start by creating the basic carousel layout with the HTML structure and navigation buttons.
Slider Component
Create Slider.jsx in the components/Slider folder:
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 Styles
Create Slider.module.css in the same folder:
.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;
}
}App Component
Update your 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 Styles
Update App.css:
.item {
height: 310px;
border-radius: 16px;
background: #FFF1D5;
display: flex;
align-items: center;
justify-content: center;
color: #604652;
font-size: 102px;
}At this point, you'll see a basic layout with navigation buttons, but the carousel won't function yet. Let's add the logic to make it work.
Carousel Logic
Now let's add the functionality to make the carousel interactive by calculating slide widths and handling navigation.
Updated Slider Component
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,
}) => {
const [slidesPerView, setSlidesPerView] = useState(initialSliderPerView);
const [slidesPerGroup, setSlidesPerGroup] = useState(initialSliderPerGroup);
const [spaceBetween, setSpaceBetween] = useState(initialSpaceBetween);
const [sliderItemWidth, setSliderItemWidth] = useState(0);
const sliderContainerRef = useRef(null);
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(div => {
div.style.width = sliderItemWidth + 'px';
});
setSliderItemWidth(sliderItemWidth);
});
}
}, [slidesPerView, spaceBetween]);
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;Updated App Component
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;Updated App Styles
.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;
}Making it Functional
Now let's add the actual sliding functionality using state management and CSS transforms.
Custom Hook: useInView
Create useInView.js in the hooks folder:
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;Updated Slider Component with Functionality
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);
const [activeSlideIndex, setActiveSlideIndex] = useState(0);
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,
});
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]);
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;
};
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;AutoPlay & Loop
Let's add autoplay functionality and infinite looping to make the carousel more dynamic.
Updated Slider Component with AutoPlay and Loop
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,
loop,
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);
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);
const sliderItems = loop
? [
...items.slice(-slidesPerView),
...items,
...items.slice(0, slidesPerView),
]
: items;
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]);
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 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;Updated App Component
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
Now let's make the carousel responsive by adding breakpoint support and window dimension tracking.
Custom Hook: useWindowDimensions
Create useWindowDimensions.js in the hooks folder:
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;
}Final Responsive Slider Component
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);
const { width: deviceWidth } = useWindowDimensions();
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]);
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]);
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;Final App Component
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;Complete Implementation
You now have a fully functional, responsive carousel component with the following features:
Key Features
- Responsive Design: Adapts to different screen sizes using breakpoints
- AutoPlay: Automatically slides to the next item at specified intervals
- Infinite Loop: Seamlessly loops back to the beginning when reaching the end
- Smooth Transitions: CSS transitions for smooth sliding animations
- Navigation Controls: Previous/Next buttons for manual navigation
- Customizable: Configurable slides per view, spacing, and grouping
Props Reference
| Prop | Type | Default | Description |
|---|---|---|---|
slidesPerView | number | 4 | Number of slides visible at once |
slidesPerGroup | number | 4 | Number of slides to move per navigation |
spaceBetween | number | 16 | Space between slides in pixels |
loop | boolean | false | Enable infinite looping |
autoPlay | boolean | false | Enable automatic sliding |
autoPlayInterval | number | 2000 | Interval for autoplay in milliseconds |
breakpoints | object | - | Responsive breakpoint configuration |
items | array | - | Array of items to display |
children | ReactNode | - | React children to display |
Breakpoints Configuration
breakpoints={{
320: {
slidesPerView: 1,
slidesPerGroup: 1,
},
768: {
slidesPerView: 2,
slidesPerGroup: 2,
},
1366: {
slidesPerView: 4,
slidesPerGroup: 4,
spaceBetween: 18
},
}}This carousel component provides a solid foundation that you can extend with additional features like pagination dots, touch/swipe support, or keyboard navigation. The modular approach makes it easy to maintain and customize for different use cases.
Summary
Building a carousel component from scratch teaches valuable lessons about:
- State Management: Handling complex state transitions
- DOM Manipulation: Direct DOM access for measurements
- CSS Transforms: Using transforms for smooth animations
- Responsive Design: Adapting to different screen sizes
- Custom Hooks: Creating reusable logic with hooks
- Performance: Optimizing re-renders and transitions
This implementation demonstrates modern React patterns and provides a production-ready carousel component that you can use in your projects or as a learning reference for building similar components.
Advanced Autocomplete Search Component
Master machine coding by creating a production-ready autocomplete search with debouncing, keyboard navigation, virtual scrolling, and real-time suggestions.
Countdown Timer Component
Master the art of building countdown timers in React using useEffect hook - perfect for quiz apps, games, and time-sensitive applications.