When you have a lot of images that you want your visitors to see, an image gallery is a great tool. I use galleries a lot on "Place" pages on this site. For example:
Not that many design systems have an image gallery component though. Material UI did not - so for https://gatsbybricks.com/, I used the React Image Lightbox package. This package worked well and had good accessibility, but it was a little bit conflict-y with the design system.
For this website, I decided to see if I could build an image gallery UI using just the building blocks provided by the design system in use (Grommet).
It wasn't as hard as you might think, but not as easy as I hoped. I put it together in about 100 lines of React.
The following Grommet concepts played a part:
The only difficult part about this was getting the "lightbox" interaction to load neatly. I had to fiddle a bit but the native onLoad image property does solve this. The trick is:
Here is the code:
import React, { useState } from "react";
import PropTypes from "prop-types";
import { Box, Layer, Image, Grid, Text, Stack, Keyboard } from "grommet";
import { Close } from "grommet-icons";
import SandboxComponent from "./SandboxComponent";
/**
* Intermal component: A thumbnail for one image in the gallery.
*/
const Thumbnail = ({ image }) => {
return <Image fit="cover" src={image.urls.thumbnail} />;
};
/**
* Internal component: the stacked large image, caption and close button.
*/
const ImageWithCaption = ({ image: { id, urls, caption }, closeHandler }) => {
const [imageLoaded, setImageLoaded] = useState(false);
const imageUrl = urls.full;
return (
<Box flex={true} justify="center">
<Stack anchor="top-right">
<Box width="large">
<Image
fit="cover"
fill={true}
src={imageUrl}
style={!imageLoaded ? { display: "none" } : null}
onLoad={() => setImageLoaded(true)}
/>
{imageLoaded && (
<Box pad="medium">
<Text>{caption}</Text>
</Box>
)}
</Box>
{imageLoaded && (
<Box
margin="large"
background={{ color: "dark-1" }}
opacity="medium"
pad="small"
>
<Close size="large" onClick={closeHandler} cursor="pointer" />
</Box>
)}
</Stack>
</Box>
);
};
/**
* Exported component: the image gallery.
*/
const ImageGallery = ({ images }) => {
const [activeImageId, setActiveImageId] = useState(null);
/**
* Determines whether the current image is active.
*
* @param object image
* A Sanity image object.
*
* @returns boolean
* Whether the current image is active.
*/
const imageIsActive = (image) => {
return activeImageId === image.id && activeImageId !== null;
};
/**
* Moves to the next or previous image.
*
* @param String direction
* Either "next" or "prev"
* @param object currentImage
* The current Sanity image object that we are navigating from.
*/
const navigate = (direction, currentImage) => {
// Find next image
const currentIndex = images.findIndex(
(image) => image.id === currentImage.id
);
const targetImage =
images[direction === "next" ? currentIndex + 1 : currentIndex - 1];
if (targetImage) {
setActiveImageId(targetImage.id);
}
};
return (
<Grid
gap="small"
columns={{ count: 3, size: "xsmall" }}
margin={{ vertical: "medium" }}
>
{images.map((image) => (
<div key={image.id}>
{imageIsActive(image) && (
<Layer
onClickOutside={() => setActiveImageId(null)}
animate={false}
>
<Keyboard
onRight={() => navigate("next", image)}
onLeft={() => navigate("prev", image)}
onEsc={() => setActiveImageId(null)}
target="document"
>
<ImageWithCaption
image={image}
closeHandler={() => setActiveImageId(null)}
/>
</Keyboard>
</Layer>
)}
<Box onClick={() => setActiveImageId(image.id)}>
<Thumbnail image={image} />
</Box>
</div>
))}
</Grid>
);
};
ImageGallery.propTypes = {
images: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.String,
caption: PropTypes.String,
urls: PropTypes.shape({
full: PropTypes.String,
thumbnail: PropTypes.String
})
})
)
};
const images = [
{
id: "1",
caption: "First image",
urls: {
thumbnail: "https://via.placeholder.com/200",
full: "https://via.placeholder.com/1600x900"
}
},
{
id: "2",
caption: "Second image",
urls: {
thumbnail: "https://via.placeholder.com/200",
full: "https://via.placeholder.com/1600x900"
}
},
{
id: "3",
caption: "Third image",
urls: {
thumbnail: "https://via.placeholder.com/200",
full: "https://via.placeholder.com/1600x900"
}
},
{
id: "4",
caption: "Fourth image",
urls: {
thumbnail: "https://via.placeholder.com/200",
full: "https://via.placeholder.com/1600x900"
}
}
];
export default () => (
<SandboxComponent>
<ImageGallery images={images} />
</SandboxComponent>
);
I think this gallery is a pretty good example of how you can use Grommet to make cool interactions, the way you want.
Next I would like to