How to build an Auto-Playing Slideshow with React

Omar Benseddik's photo
Omar Benseddik
Updated 2023-11-13 · 7 min
Table of contents

In this article we'll build an auto-playing slideshow using React.

The article is divided into two sections:

  1. The trick
  2. Functionality

Here's the final result (Codepen link here):

The trick

Our Slideshow component is divided in three containers:

  • slideshow
  • slideshowSlider
  • slide

Here's a sketch to visualize the structure:

Slideshow structure 1
Slideshow structure 1

What is visible to the user is what is shown within the red box (the container slideshow).

After a couple of seconds, the container slideshowSlider will move to the left to expose the next container slide, as shown below:

Slideshow structure 1
Slideshow structure 1

As you can imagine, after a couple of seconds the container slideshowSlider will move again and what will be shown to the user is the yellow container slide.

A couple of seconds later, the container slideshowSlider will go back to its original position and we'll see the blue container slide again.

And so on.

Here's the corresponding markup:

function Slideshow() {
return (
<div className="slideshow">
<div className="slideshowSlider">
<div className="slide"></div>
</div>
</div>
);
}

Step 1: show colored slides

Let's use the previous markup to show a few colored slides:

const colors = ["#0088FE", "#00C49F", "#FFBB28"];
function Slideshow() {
return (
<div className="slideshow">
<div className="slideshowSlider">
{colors.map((backgroundColor, index) => (
<div className="slide" key={index} style={{ backgroundColor }}/>
))}
</div>
</div>
);
}

Step 2: styling

First, let's style the parent container slideshow:

/* Slideshow */
.slideshow {
margin: 0 auto;
overflow: hidden;
max-width: 500px;
}

We center it with margin: 0 auto, set a max-width to it and make the content flowing outside the element's box invisible with overflow:hidden.

Now let's style slide:

/* Slideshow */
.slideshow {
margin: 0 auto;
overflow: hidden;
max-width: 500px;
}
.slide {
height: 400px;
width: 100%;
border-radius: 40px;
}

We get:

Slides are on top of each other
Slides are on top of each other

We don't want to have the slides one on top of each other, but we want them one next to each other.

For that, we'll set display: inline-block since divs are set with display:block by default, which makes them start in a new line:

/* Slideshow */
.slideshow {
margin: 0 auto;
overflow: hidden;
max-width: 500px;
}
.slide {
display: inline-block;
height: 400px;
width: 100%;
border-radius: 40px;
}

We get:

Not much changed
Not much changed

Not much changed, and it still looks like we have display:block and that is because divs wrap to the next line when there's no space in the container. Because our slides take 100% of the slideshow's width each, there is no space in the container.

We'll use white-space: nowrap in the slides container so we never wrap to the next line:

/* Slideshow */
.slideshow {
margin: 0 auto;
overflow: hidden;
max-width: 500px;
}
.slideshowSlider {
white-space: nowrap;
}
.slide {
display: inline-block;
height: 400px;
width: 100%;
border-radius: 40px;
}

We get:

No more wrapping to the next line
No more wrapping to the next line

We no longer have divs wrapping to the next line.

Step 3: create the buttons

Now that we have the structure of the color containers, let's add the buttons (dots) beneath them.

We'll map again through the array again and add a dot for each array element:

const colors = ["#0088FE", "#00C49F", "#FFBB28"];
function Slideshow() {
return (
<div className="slideshow">
<div className="slideshowSlider">
{colors.map((backgroundColor, index) => (
<div
className="slide"
key={index}
style={{ backgroundColor }}
></div>
))}
</div>
<div className="slideshowDots">
{colors.map((_, idx) => (
<div key={idx} className="slideshowDot"></div>
))}
</div>
</div>
);
}

Let's style the buttons:

/* Slideshow */
.slideshow {
margin: 0 auto;
overflow: hidden;
max-width: 500px;
}
.slideshowSlider {
white-space: nowrap;
}
.slide {
display: inline-block;
height: 400px;
width: 100%;
border-radius: 40px;
}
/* Buttons */
.slideshowDots {
text-align: center;
}
.slideshowDot {
display: inline-block;
height: 20px;
width: 20px;
border-radius: 50%;
cursor: pointer;
margin: 15px 7px 0px;
background-color: #c4c4c4;
}

We get:

Color container and buttons (dots) are ready
Color container and buttons (dots) are ready

We are done with the structure and the styling. Let's now focus on the functionality of the slideshow.

Functionality

If you noticed in the sketch above, we moved the position of slideshowSlider to the left to display different color containers in its parent div slideshow.

Notice how the blue container below is moving to the left as a result of slideshowSlider moving:

Video poster

To achieve this, we will use transform: translate3d (or you can use transform: translate).

What we essentially want to do here is move the position of slideshowSlider by 0% when index is 0, -100% when index is 1 and by -200% when index is 2.

To keep tracking of the currently displayed index, we use useState and we initialize it with 0:

const colors = ["#0088FE", "#00C49F", "#FFBB28"];
function Slideshow() {
const [index, setIndex] = React.useState(0);
return (
<div className="slideshow">
<div
className="slideshowSlider"
style={{ transform: `translate3d(${-index * 100}%, 0, 0)` }}
>
{colors.map((backgroundColor, index) => (
<div
className="slide"
key={index}
style={{ backgroundColor }}
></div>
))}
</div>
<div className="slideshowDots">
{colors.map((_, idx) => (
<div key={idx} className="slideshowDot"></div>
))}
</div>
</div>
);
}

To make the slideshow automatic, we change the index every 2,5 seconds using setTimeout.

Since this is a side effect, we do so with useEffect.

Since we want to perform this action every time the index changes, we put the index in the dependency array passed to useEffect:

const colors = ["#0088FE", "#00C49F", "#FFBB28"];
const delay = 2500;
function Slideshow() {
const [index, setIndex] = React.useState(0);
React.useEffect(() => {
setTimeout(
() =>
setIndex((prevIndex) =>
prevIndex === colors.length - 1 ? 0 : prevIndex + 1
),
delay
);
return () => {};
}, [index]);
return (
<div className="slideshow">
<div
className="slideshowSlider"
style={{ transform: `translate3d(${-index * 100}%, 0, 0)` }}
>
{colors.map((backgroundColor, index) => (
<div
className="slide"
key={index}
style={{ backgroundColor }}
></div>
))}
</div>
<div className="slideshowDots">
{colors.map((_, idx) => (
<div key={idx} className="slideshowDot"></div>
))}
</div>
</div>
);
}

Every 2500 milliseconds (2.5 seconds), the setIndex function will be called. It will first check if the current index is equal to the array's length minus one, that way it knows if to move to the next index or start from scratch.

For example, if we're at index 0, which is not equal to the array length minus one (3-1=2), it will update the index to be 1.

However, if we're at index 2, which is equal to the array's length minus one (3-1=2), it will update the index to be 0.

We get:

Video poster

We want a smoother transition, so let's go back to the CSS and add transition to slideshowSlider:

/* Slideshow */
.slideshow {
margin: 0 auto;
overflow: hidden;
max-width: 500px;
}
.slideshowSlider {
white-space: nowrap;
transition: ease 1000ms;
}
.slide {
display: inline-block;
height: 400px;
width: 100%;
border-radius: 40px;
}
/* Buttons */
.slideshowDots {
text-align: center;
}
.slideshowDot {
display: inline-block;
height: 20px;
width: 20px;
border-radius: 50%;
cursor: pointer;
margin: 15px 7px 0px;
background-color: #c4c4c4;
}

Now it's better:

Video poster

Slideshow animation is better with `transition`

The slideshow works, but the buttons are not reflecting the active slide.

So far, all our buttons are grey. Let's add a className "active" to color in purple the button corresponding to the current slide index (index state value).

While mapping through the colors, we check if the index of the slide is equal to the index of the dot, if it is the case, it takes the additional className active to reflect the change in color:

const colors = ["#0088FE", "#00C49F", "#FFBB28"];
const delay = 2500;
function Slideshow() {
const [index, setIndex] = React.useState(0);
React.useEffect(() => {
setTimeout(
() =>
setIndex((prevIndex) =>
prevIndex === colors.length - 1 ? 0 : prevIndex + 1
),
delay
);
return () => {};
}, [index]);
return (
<div className="slideshow">
<div
className="slideshowSlider"
style={{ transform: `translate3d(${-index * 100}%, 0, 0)` }}
>
{colors.map((backgroundColor, index) => (
<div
className="slide"
key={index}
style={{ backgroundColor }}
></div>
))}
</div>
<div className="slideshowDots">
{colors.map((_, idx) => (
<div
key={idx}
className={`slideshowDot${index === idx ? " active" : ""}`}
></div>
))}
</div>
</div>
);
}

Now let's add styles corresponding to the className active:

/* Slideshow */
.slideshow {
margin: 0 auto;
overflow: hidden;
max-width: 500px;
}
.slideshowSlider {
white-space: nowrap;
transition: ease 1000ms;
}
.slide {
display: inline-block;
height: 400px;
width: 100%;
border-radius: 40px;
}
/* Buttons */
.slideshowDots {
text-align: center;
}
.slideshowDot {
display: inline-block;
height: 20px;
width: 20px;
border-radius: 50%;
cursor: pointer;
margin: 15px 7px 0px;
background-color: #c4c4c4;
}
.slideshowDot.active {
background-color: #6a0dad;
}

Our buttons now reflect the changes in the slideshow:

Video poster

Now let's make them clickable, so when we click on the first dot we display the blue container, if we click on the second dot we display the green contain and if we click on the third dot we display the yellow container.

To achieve this, we change the index of the slide to be the same as the index of the button:

const colors = ["#0088FE", "#00C49F", "#FFBB28"];
const delay = 2500;
function Slideshow() {
const [index, setIndex] = React.useState(0);
React.useEffect(() => {
setTimeout(
() =>
setIndex((prevIndex) =>
prevIndex === colors.length - 1 ? 0 : prevIndex + 1
),
delay
);
return () => {};
}, [index]);
return (
<div className="slideshow">
<div
className="slideshowSlider"
style={{ transform: `translate3d(${-index * 100}%, 0, 0)` }}
>
{colors.map((backgroundColor, index) => (
<div
className="slide"
key={index}
style={{ backgroundColor }}
></div>
))}
</div>
<div className="slideshowDots">
{colors.map((_, idx) => (
<div
key={idx}
className={`slideshowDot${index === idx ? " active" : ""}`}
onClick={() => {
setIndex(idx);
}}
></div>
))}
</div>
</div>
);
}

It works, however, because we didn't clear our setTimeout, by clicking multiple times on the dots we've distorted the value of the timer:

Video poster

To avoid such scenario, we'll clear our setTimeout by using the clearTimeout method. The ID value returned by setTimeout() is used as the parameter for the clearTimeout().

We will store it in a variable and use clearTimeout() to start the timer from 0, to avoid the scenario in the GIF above.

To store the variable, we use useRef to create an object whose value is accessed with the object's current property.

const colors = ["#0088FE", "#00C49F", "#FFBB28"];
const delay = 2500;
function Slideshow() {
const [index, setIndex] = React.useState(0);
const timeoutRef = React.useRef(null);
React.useEffect(() => {
timeoutRef.current = setTimeout(
() =>
setIndex((prevIndex) =>
prevIndex === colors.length - 1 ? 0 : prevIndex + 1
),
delay
);
return () => {};
}, [index]);
return (
<div className="slideshow">
<div
className="slideshowSlider"
style={{ transform: `translate3d(${-index * 100}%, 0, 0)` }}
>
{colors.map((backgroundColor, index) => (
<div
className="slide"
key={index}
style={{ backgroundColor }}
></div>
))}
</div>
<div className="slideshowDots">
{colors.map((_, idx) => (
<div
key={idx}
className={`slideshowDot${index === idx ? " active" : ""}`}
onClick={() => {
setIndex(idx);
}}
></div>
))}
</div>
</div>
);
}

Now we'll create a function resetTimeout to clearTimeout, and it'll be called every time the index of the slide changes.

To cleanup after the effect (when the component gets destroyed), we call the resetTimeout function to clear the timeout:

const colors = ["#0088FE", "#00C49F", "#FFBB28"];
const delay = 2500;
function Slideshow() {
const [index, setIndex] = React.useState(0);
const timeoutRef = React.useRef(null);
function resetTimeout() {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
}
React.useEffect(() => {
resetTimeout();
timeoutRef.current = setTimeout(
() =>
setIndex((prevIndex) =>
prevIndex === colors.length - 1 ? 0 : prevIndex + 1
),
delay
);
return () => {
resetTimeout();
};
}, [index]);
return (
<div className="slideshow">
<div
className="slideshowSlider"
style={{ transform: `translate3d(${-index * 100}%, 0, 0)` }}
>
{colors.map((backgroundColor, index) => (
<div
className="slide"
key={index}
style={{ backgroundColor }}
></div>
))}
</div>
<div className="slideshowDots">
{colors.map((_, idx) => (
<div
key={idx}
className={`slideshowDot${index === idx ? " active" : ""}`}
onClick={() => {
setIndex(idx);
}}
></div>
))}
</div>
</div>
);
}

Now we can click on the dots as much as we want, the slideshow will still work perfectly fine:

Video poster

Recent articles

Guide to fast Next.js

Insights into how Tinloof measures website speed with best practices to make faster websites.
Seif Ghezala's photo
Seif Ghezala
2024-01-15 · 13 min