How to Make a Slideshow Gallery with ReactJS and Styled-Components

Tags: react, javascript
Published: 2021-12-04 10:19:57

A slideshow gallery is part of the visual display modes you find on the web. It helps users navigate between images by boldly showing one picture at a time, leaving the other ones available on the side.

This blog post shows you how you can build a full-viewport slideshow gallery.

### PREREQUISITES

- Basic knowledge of JavaScript, React and styled-components

> The complete code is [in this repo](https://github.com/nazifbara/how-to-reactjs).

## Layout of a slideshow gallery

What will be the structure of our slideshow? I got us covered with the following wireframe:

![slideshow wireframe](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hrl3q5csr92gleruel3v.png)

## The Slide Wrapper

From our wireframe, we see that a container wraps all the elements. So first, let's create a `SlideWrapper` styled component:

```jsx
// src/slideshow-gallery/index.js
import styled from 'styled-components';

const View = () => <Slideshow />;

const Slideshow = () => {
  return <SlideWrapper></SlideWrapper>;
};

const SlideWrapper = styled.div`
  position: relative;
  width: 100vw;
  height: 100vh;
`;

export default View;
```

The `SlideWrapper` occupies the entire viewport's width and height. We wanted a full-viewport slideshow, right? And note that the children will position themselves relative to this wrapper, hence the `position: relative;`.

## The Image Box

Each selected image will be in a box that preserves the image ratio (width/height). So let's create a wrapper around an `<img>` tag called `ImageBox`. This wrapper will put the image into a constraint. That is, the image must stay within the delimitation of the wrapper. That way, our slideshow will remain stable no matter the image size and orientation.

![image box concept](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ecssss5bzp1kxngkp0v2.png)

In the following, we define and use the `ImageBox` component:

```jsx
// src/slideshow-gallery/index.js
// ...
const ImageBox = styled.div`
  position: relative;
  background-color: #343434;
  width: 100%;
  height: 85%;

  img {
    position: absolute;
    margin: auto;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    max-width: 100%;
    max-height: 100%;
  }
`;

const Slideshow = () => {
  return (
    <SlideWrapper>
      <ImageBox>
        <img alt="" src="/pathToAnImage" />
      </ImageBox>
    </SlideWrapper>
  );
};
//...
```

Here is the result with different image orientations and sizes:

![image box result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3oguy7psu6kgz60iyjbs.png)

Our `ImageBox` needs a left and right button to help us switch between images. So let's create a `NavButton` styled component to accomplish that:

```jsx
// src/slideshow-gallery/index.js
import styled, { css } from 'styled-components';

import rurikoTempleImage from './assets/ruriko-in-temple.jpeg';
import { ReactComponent as ChevronLeft } from './assets/chevron-left.svg';
import { ReactComponent as ChevronRight } from './assets/chevron-right.svg';

// ...

const Slideshow = () => {
  return (
    // ...
    <ImageBox>
      <img alt="" src={rurikoTempleImage} />
      <NavButton position="left">
        <ChevronLeft />
      </NavButton>
      <NavButton position="right">
        <ChevronRight />
      </NavButton>
    </ImageBox>
    // ...
  );
};

const NavButton = styled.button`
  cursor: pointer;
  position: absolute;
  top: 45%;
  padding: 5px;
  border-radius: 3px;
  border: none;
  background: rgba(255, 255, 255, 0.7);

  ${({ position }) =>
    position === 'left' &&
    css`
      left: 10px;
    `}

  ${({ position }) =>
    position === 'right' &&
    css`
      right: 10px;
    `}
`;

// ...
```

The `NavButton` is vertically centered in the `ImageBox` (`top: 45%;` ). Based on the position prop, the `NavButton` is either positioned to the left or the right.

![Nav Buttons](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f6581hfnh1op1572cxfc.png)

It would also be nice to have a caption at the bottom:

```jsx
// src/slideshow-gallery/index.js
const Slideshow = () => {
  return (
    <SlideWrapper>
      <ImageBox>
        // ...
        <ImageCaption>Ruriko Temple</ImageCaption>
      </ImageBox>
    </SlideWrapper>
  );
};

// ...

const ImageCaption = styled.span`
  width: 100%;
  text-align: center;
  font-weight: bold;
  position: absolute;
  bottom: 0;
  padding: 8px;
  background: rgba(255, 255, 255, 0.7);
`;

// ...
```

And we get the following:

![Image caption](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kpsefvqt2p9de0zhfhlc.png)

## Getting the Slideshow Items as prop

The slideshow needs to get a set of images from the outside. The `src/slideshow-gallery/data.js` file export an array of pictures that we can use. Each item gives access to the image source as well as the image caption:

```js
// src/slideshow-gallery/data.js
import rurikoTemple from './assets/ruriko-in-temple.jpeg';
import itsukushimaShrine from './assets/itsukushima-shrine.jpeg';
// ...
const slideItems = [
  {
    image: nemichiJinja,
    caption: 'Nemichi-Jinja, Seki',
  },
  {
    image: itsukushimaShrine,
    caption: 'Itsukushima Shrine',
  },
  // ...
];

export default slideItems;
```

Let's import this array and pass it down to the `Slideshow` component:

```jsx
// src/slideshow-gallery/index.js
// ...
import data from './data';

const View = () => <Slideshow items={data} />;
// ...
```

As our `Slideshow` component will render differently based on the selected image, we need to use a state. This state will hold all of the slide items in addition to the index of the currently active item:

```jsx
// src/slideshow-gallery/index.js
import { useState } from 'react';
// ...
const Slideshow = (props) => {
  const [{ items, activeIndex }, setState] = useState({
    items: props.items,
    activeIndex: 0, // begin with the first item
  });

  return (
    <SlideWrapper>
      <ImageBox>
        <img alt={items[activeIndex].caption} src={items[activeIndex].image} />
        <NavButton position="left">
          <ChevronLeft />
        </NavButton>
        <NavButton position="right">
          <ChevronRight />
        </NavButton>
        <ImageCaption>{items[activeIndex].caption}</ImageCaption>
      </ImageBox>
    </SlideWrapper>
  );
};
// ...
```

## Navigating between Images

With the state in place, we can add a click handler function to each `NavButton` to change the image:

```jsx
// src/slideshow-gallery/index.js
// ...
const Slideshow = (props) => {
  // ...
  const moveTo = (newIndex) => () => {

    if (newIndex === -1) {
      // jump from the first image to the last
      setState((s) => ({ ...s, activeIndex: items.length - 1 }));
      return;
    }
    if (newIndex === items.length) {
      // jump from the last image to the first
      setState((s) => ({ ...s, activeIndex: 0 }));
      return;
    }

    setState((s) => ({ ...s, activeIndex: newIndex }));
  };

  return (
    <SlideWraper>
        // ...
        <NavButton position="left" onClick={moveTo(activeIndex - 1)}>
        // ...
        <NavButton position="right" onClick={moveTo(activeIndex + 1)}>
        // ...
    </SlideWraper>
  );
};
// ...
```

## Thumbnail Images

After the `ImageBox`, we want a list of thumbnails for all of our images. That list will show the active image thumbnail with 100% opacity. And the inactive ones will be 40% transparent.

```jsx
// src/slideshow-gallery/index.js
// ...
const Slideshow = (props) => {
  // ...
  return (
    <SlideWraper>
      // ...
      </ImageBox>
      <ThumbnailList>
        {items.map((item, index) => (
          <Thumbnail active={activeIndex === index} src={item.image} />
        ))}
      </ThumbnailList>
    </SlideWraper>
  );
};

const ThumbnailList = styled.div`
  display: flex;
  align-items: stretch;
  width: 100%;
  height: 15%;
`;
const Thumbnail = styled.div`
  cursor: pointer;
  opacity: ${({ active }) => (active ? 1 : 0.6)};
  background-image: url(${({ src }) => src});
  background-size: cover;
  background-position: center;
  flex-grow: 1;

  :hover {
    opacity: 1;
  }
`;
// ...
```

Finally, we want to jump directly to an image by clicking on its thumbnail. To do that, we reuse our `moveTo` function:

```jsx
// src/slideshow-gallery/index.js
// ...
{
  items.map((item, index) => (
    <Thumbnail
      onClick={moveTo(index)}
      // ...
    />
  ));
}
// ...
```

And now, the slideshow gallery is ready! Take a look at the final result:

![slideshow gallery](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/302hm0mrtloaiqwruuaf.png)

## Conclusion

From a wireframe, we broke down the distinct parts of the slideshow. It was the cornerstone we built upon until the final UI. 

You can pat yourself on the back for making it to the end.

Thanks for reading!