This article was originally published on Tinloof.
In this article we'll build an auto-playing slideshow using React.
The article is divided in two sections:
- The trick
- Functionality
Here's the final result (Codepen link here and Codesandbox link here):
The trick
Our Slideshow component is divided in three containers:
- slideshow
- slideshowSlider
- slide
Here's a sketch to visualize the structure:
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:
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:
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 div
s 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, and it still looks like we have display:block
and that is because div
s 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:
We no longer have div
s 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:
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:
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:
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:
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:
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:
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:
Learn more React
Learning by doing is the best thing. Here are more Tinloof React tutorials:
Note: Tinloof is a Berlin-based product studio. Get in touch if you'd like us to help you build slick and fast web applications.
Top comments (0)