Creating a Reusable Carousel Component in React

In this article, I will be showcasing how we can create a simple carousel component without need of any external library. I will create a barebones carousel component, to which we can apply styling based on our requirement.
What is a Carousel?
A carousel is used to display content in a slideshow like format. The content can be anything like images or cards. It’s main purpose is to display a bunch of content of similar type in a confined space. It can be used to display banners or popular products on a website and many more. The main advantage is that it saves space while making the UI look and feel dynamic.
Destructuring a Carousel
Basically, a carousel is an element which arranges the content inside it in a vertical or horizontal manner. It can accept a long list of items, but only shows a limited amount at a time. The remaining items are carefully hidden by manipulating the overflow properties of the parent container.
Prerequistes
To continue further, you will need the knowledge of React. Additionally, I will be using tailwindCss and typescript for the tutorial.
Let’s get started
Setup
First get your basic react project setup using any of the tools you like, I will be using vite. Once, the setup is done we will add tailwind for styling and lucide-react for icons. Also, add a carousel.tsx
file inside the components
folder.
I will also be using the cn
function from shadcn/ui
which uses tailwind-merge
and clsx
libraries for handling and merging tailwind classes. You can easily google it or use AI to learn more about it.
This is where we will be working. With this the project structure after the setup is done.
└── src/
├── App.css
├── App.tsx
├── index.css
├── main.tsx
├── vite-env.d.ts
└── components/
└── carousel.tsx
└── lib/
└── utils.ts
Creating the Carousel component
Initially, we will add a simple div
element with properties flex
and allowing it to overflow. This will let us add as many elements as we want in a horizontal alignment. The snap
classes will help in properly snapping the children at a specific point.
import React from "react";
interface CarouselProps {
children: React.ReactNode;
}
function Carousel({ children }: CarouselProps) {
return <div className="w-full overflow-x-scroll flex snap-x snap-mandatory">{children}</div>;
}
Carousel.displayName = "Carousel";
export default Carousel;
In the same manner, we will also add a CarouselItem
component. This will be the individual slide / images in our carousel component.
import { cn } from "../lib/util";
function CarouselItem({ children, className }: React.ComponentProps<"div">) {
return <div className={cn("snap-start", className)}>{children}</div>;
}
CarouselItem.displayName = "CarouselItem";
export { CarouselItem };
Usage
This is how we can use the component.
<div className="m-4">
<Carousel>
<CarouselItem className="min-w-[500px] p-4 border-2 mx-5">
Slide 1
</CarouselItem>
<CarouselItem className="min-w-[500px] p-4 border-2 mx-5">
Slide 2
</CarouselItem>
<CarouselItem className="min-w-[500px] p-4 border-2 mx-5">
Slide 3
</CarouselItem>
<CarouselItem className="min-w-[500px] p-4 border-2 mx-5">
Slide 4
</CarouselItem>
<CarouselItem className="min-w-[500px] p-4 border-2 mx-5">
Slide 5
</CarouselItem>
</Carousel>
</div>
Demo:
Currently, we can see that slides are snapping at the start position correctly but we are required to scroll through the slides using our mouse pointer. That is not the intended behaviour for a carousel. Next, we will work on adding the expected functionality to this component.
Adding functionality
First we will hide the scrollbar. For this we need to use CSS:
.hide-scrollbar {
scrollbar-width: none;
/* Firefox */
-ms-overflow-style: none;
/* IE and Edge */
}
.hide-scrollbar::-webkit-scrollbar {
display: none;
/* Chrome, Safari, Opera */
}
add this hide-scrollbar
class to our parent Carousel
component.
function Carousel({ children }: CarouselProps) {
return <div className="w-full overflow-x-scroll flex snap-x snap-mandatory hide-scrollbar">{children}</div>;
}
Now, that scrollbar is hidden, we need to add the following things:
Adding slides per view
Adding prev and next buttons
Getting active slide
Ok then let’s get started!
Adding slides per view
Till now we have passed a min-height
to the carousel-item
but this approach does not garuntee a fixed amount of slides on different size of screens. For this we will need to use some javascript magic.
First we will need to update our props to accepts slides
, this will specify how many number of slides we want to specify in our carousel at any given time. Then, we will need a function to calculate the width
for the carousel-items
. Here, is the code for it:
const containerWidth = parentContainer.current.offsetWidth;
const gap = 32; // gap between any two slides
const totalGap = gap * (slides - 1); // total width that will occupied by all gaps between between any two slides
const availableWidth = containerWidth - totalGap;
const width = availableWidth / slides; // width to apply to one slide
The code block calculates width to be applied to each carousel-item
from the total width
of the carousel component. For now, I have kept the gap as 32px
but you can make it dynamic, by accepting it in props.
Here, is the code for updated carousel
component:
interface CarouselProps {
children: React.ReactNode;
slides: number;
}
function Carousel({ children, slides }: CarouselProps) {
const parentContainer = useRef<HTMLDivElement>(null);
const [itemWidth, setItemWidth] = useState(0);
useEffect(() => {
if (parentContainer.current && slides) {
const containerWidth = parentContainer.current.offsetWidth;
const gap = 32; // gap between any two slides
const totalGap = gap * (slides - 1); // total width that will occupied by all gaps between any two slides
const availableWidth = containerWidth - totalGap;
const width = availableWidth / slides; // width to apply to one slide
setItemWidth(width);
}
}, [slides, children]);
return (
<div
ref={parentContainer}
className="w-full overflow-x-scroll flex snap-x snap-mandatory hide-scrollbar"
style={{
gap: 32,
}}
>
{React.Children.map(children, (child) =>
React.isValidElement(child) ? (
<div style={{ minWidth: `${itemWidth}px` }}>{child}</div>
) : null
)}
</div>
);
}
We have created a reference to the parent-container
, which will be used to get the width of the container. The useEffect
calculates the width for each individual slide and stores it in a itemWidth
state variable.
Adding prev and next buttons
Now, let’s add controls for going to previous and next slides. For this, we will only need to programatically scroll forward and backward in our container.
const scrollBy = (direction: number) => {
if (parentContainer.current && itemWidth) {
parentContainer.current.scrollTo({
left: parentContainer.current.scrollLeft + direction * itemWidth,
behavior: "smooth",
});
}
};
The scrollBy
function will scroll in both directions based on the width of our slides. If we pass positive number it will scroll to right and if negative number is passed it will scroll to left.
Now, to create controls, I will use the hook useImperativeHandle
, this hook is used to extend the functionality of ref. You can read more about it on the official react docs. And we will also need to wrap our carousel
component in a forwardRef
. This will enable us to use the prev and next methods outside of our component.
This will be the updated code:
export interface SliderRefType {
next: () => void;
prev: () => void;
}
interface CarouselProps {
children: React.ReactNode;
slides: number;
}
const Carousel = forwardRef<SliderRefType, CarouselProps>(
({ children, slides }, ref) => {
const parentContainer = useRef<HTMLDivElement>(null);
const [itemWidth, setItemWidth] = useState(0);
useEffect(() => {
if (parentContainer.current && slides) {
const containerWidth = parentContainer.current.offsetWidth;
const gap = 32; // gap between any two slides
const totalGap = gap * (slides - 1); // total width that will occupied by all gaps between any two slides
const availableWidth = containerWidth - totalGap;
const width = availableWidth / slides; // width to apply to one slide
setItemWidth(width);
}
}, [slides, children]);
useImperativeHandle(ref, () => ({
next: () => scrollBy(1),
prev: () => scrollBy(-1),
}));
const scrollBy = (direction: number) => {
if (parentContainer.current && itemWidth) {
parentContainer.current.scrollTo({
left: parentContainer.current.scrollLeft + direction * itemWidth,
behavior: "smooth",
});
}
};
return (
<div
ref={parentContainer}
className="w-full overflow-x-scroll flex snap-x snap-mandatory hide-scrollbar"
style={{
gap: 32,
}}
>
{React.Children.map(children, (child) =>
React.isValidElement(child) ? (
<div style={{ minWidth: `${itemWidth}px` }}>{child}</div>
) : null
)}
</div>
);
}
);
Inside the useImperativeHandle
, we are passing two functions prev
and next
which we will use in our previous and next buttons.
Here, is the usage of the updated component:
function App() {
const carouselRef = useRef<CarouselRefType>(null);
const handlePrev = () => {
if (!carouselRef.current) return;
carouselRef.current.prev();
};
const handleNext = () => {
if (!carouselRef.current) return;
carouselRef.current.next();
};
return (
<div className="m-4">
<Carousel ref={carouselRef} slides={3}>
<CarouselItem className="p-4 border-2">Slide 1</CarouselItem>
<CarouselItem className="p-4 border-2">Slide 2</CarouselItem>
<CarouselItem className="p-4 border-2">Slide 3</CarouselItem>
<CarouselItem className="p-4 border-2">Slide 4</CarouselItem>
<CarouselItem className="p-4 border-2">Slide 5</CarouselItem>
</Carousel>
<div className="flex items-center justify-center gap-4 my-8">
<button
onClick={handlePrev}
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
>
Prev
</button>
<button
onClick={handleNext}
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
>
Next
</button>
</div>
</div>
);
}
As you can see, using the ref
to the carousel
component, we are able to access the prev
and next
functions of the carousel.
Getting Active Slide
To get the active slide, we can create a state variable which will be updated every time a scroll occurs. Basically, we will add an event listener
which will update our variable.
const updateActiveSlide = useCallback(() => {
if (parentContainer.current && itemWidth) {
const index = Math.round(parentContainer.current.scrollLeft / itemWidth);
if (onActiveSlideChange) onActiveSlideChange(index);
}
}, [itemWidth]);
useEffect(() => {
const container = parentContainer.current;
if (!container) return;
container.addEventListener("scroll", updateActiveSlide, {
passive: true,
});
return () => {
container.removeEventListener("scroll", updateActiveSlide);
};
}, [updateActiveSlide]);
We can add the above code block in our carousel component. Whenever, a scroll occurs it will call the updateActiveSlide
function, to update the active slide. We can create an onActiveSlideChange
prop to be able to access our active-slide
outside of the main carousel component.
interface CarouselProps {
children: React.ReactNode;
slides: number;
onActiveSlideChange?: (index: number) => void;
}
Here is the usage:
function App() {
const carouselRef = useRef<CarouselRefType>(null);
const [active, setActive] = useState(0);
useEffect(() => {
console.log(active);
}, [active]);
const handlePrev = () => {
if (!carouselRef.current) return;
carouselRef.current.prev();
};
const handleNext = () => {
if (!carouselRef.current) return;
carouselRef.current.next();
};
return (
<div className="m-4">
<Carousel ref={carouselRef} slides={3} onActiveSlideChange={setActive}>
<CarouselItem className="p-4 border-2">Slide 1</CarouselItem>
<CarouselItem className="p-4 border-2">Slide 2</CarouselItem>
<CarouselItem className="p-4 border-2">Slide 3</CarouselItem>
<CarouselItem className="p-4 border-2">Slide 4</CarouselItem>
<CarouselItem className="p-4 border-2">Slide 5</CarouselItem>
</Carousel>
<div className="flex items-center justify-center gap-4 my-8">
<button
onClick={handlePrev}
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
>
Prev
</button>
<button
onClick={handleNext}
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
>
Next
</button>
</div>
</div>
);
}
Wrapping up
Thats it! It was that simple to build a simple carousel component. It is a basic implementation but we can add features like auto-slide, responsive slides per view and orientation and many more. As this blog is getting long, I will be writing about them in my next blog. Till then If you liked reading the blog, consider giving a follow.
Thanks for Reading!!! Happy Learning!!!
Subscribe to my newsletter
Read articles from Kalpak Goshikwar directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
