DEV Community

Omar
Omar

Posted on • Updated on • Originally published at tinloof.com

How to build an Auto-Playing Slideshow with React

This article was originally published on Tinloof.


Final result

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

The article is divided in two sections:

  1. The trick
  2. 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:

https://res.cloudinary.com/https-tinloof-com/image/upload/v1605382983/blog/automated-slideshow-react/position_1_itid9p.png

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:

https://res.cloudinary.com/https-tinloof-com/image/upload/v1605382983/blog/automated-slideshow-react/position_2_lv9t56.png

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}

Enter fullscreen mode Exit fullscreen mode

Step 2: styling

First, let's style the parent container slideshow:

/* Slideshow */

.slideshow {
  margin: 0 auto;
  overflow: hidden;
  max-width: 500px;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

We get:

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;
}
Enter fullscreen mode Exit fullscreen mode

We get:


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;
}
Enter fullscreen mode Exit fullscreen mode

We get:


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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

We get:


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:

Logic behind the slideshow's functionality

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}

Enter fullscreen mode Exit fullscreen mode

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:

Slideshow is working

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;
}
Enter fullscreen mode Exit fullscreen mode

Now it's better:

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>
  );
}

Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

Our buttons now reflect the changes in the slideshow:

Buttons do now reflect the changes

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>
  );
}

Enter fullscreen mode Exit fullscreen mode

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:

The timer value is not cleared

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>
  );
}

Enter fullscreen mode Exit fullscreen mode

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>
  );
}

Enter fullscreen mode Exit fullscreen mode

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

The timer value is now cleared

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)