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.

Layout of a slideshow gallery

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

slideshow wireframe

The Slide Wrapper

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

// 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

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

// 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

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:

// 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

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

// 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

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:

// 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:

// 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:

// 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:

// 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.

// 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:

// 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

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!