DEV Community

Cover image for Creating an Interactive Image Gallery with HTML and CSS
Alvaro Montoro
Alvaro Montoro

Posted on • Originally published at alvaromontoro.com

Creating an Interactive Image Gallery with HTML and CSS

The other day, while navigating online looking for inspiration, I found a photo gallery design by kevin on Dribbble, so I decided to create a minimal version of it (you can see a live demo on CodePen or at the end of the article.)

Capture of a photo gallery with elements distributed in files of 3 and 2

The HTML Structure

The HTML will be limited to just a container and eight photos for simplicity. This choice is just for the demo. The idea is for the code to be extendable to any number of pictures or elements –e.g., buttons or links containing the images or having them as backgrounds.



<article class="grid-gallery">
  <img src="./pic-1.webp" alt="description of picture 1" />
  <img src="./pic-2.webp" alt="description of picture 2" />
  <img src="./pic-3.webp" alt="description of picture 3" />
  <img src="./pic-4.webp" alt="description of picture 4" />
  <img src="./pic-5.webp" alt="description of picture 5" />
  <img src="./pic-6.webp" alt="description of picture 6" />
  <img src="./pic-7.webp" alt="description of picture 7" />
  <img src="./pic-8.webp" alt="description of picture 8" />
</article>


Enter fullscreen mode Exit fullscreen mode

Instead of using <img> tags, we could use buttons or links, and the CSS change would require only minor modifications. Again, for simplicity, we'll go with only images.

Setting the Stage: Styling the Grid

To create the effect of the intertwined pictures, we will use a grid with six columns. This may seem counterintuitive because we have rows with 3 or 2 photos. But it makes sense that each image will occupy two columns, and the rows with only two pictures will be "shifted" by one column (more on this soon). Also, the rows will be half the size of the image, making the grid cells squared.

Screenshot of a photo gallery with grid lines on top

While we know the number of columns, the number of rows will depend on the number of photos in the gallery… and we don't know it. Therefore, instead of specifying a template, we will define their default size and let the browser deal with them as they are added.

This is the CSS code for the container:



.grid-gallery {
  --size: 100px;
  display: grid;
  grid-template-columns: repeat(6, var(--size));
  grid-auto-rows: var(--size);
  gap: 5px;
  place-items: start center;
  margin-bottom: var(--size);
}


Enter fullscreen mode Exit fullscreen mode

Let's see what each of those properties do:

  • --size: 100px; Specifies what will be the size of the cell in the grid.
  • display: grid; This indicates that we will be using a grid.
  • grid-template-columns: repeat(6, var(--size)); Defines the number and size of the columns: 6 columns of 100px each.
  • grid-auto-rows: var(--size); Each column added to the grid will have the defined cell height.
  • gap: 5px; It is the distance between the cells and, therefore, the photos.
  • place-items: start center; Specifies how the images will be aligned within the cell: vertically, to the top, and horizontally, in the center.
  • margin-bottom: var(--size); This code fixes an issue with the gallery: the photos occupy the size of two cells, so the last row of images will overflow the cell and potentially overlap the content below. To avoid it, we add a bottom margin equal to the size of an extra row.

With this, we have set the stage for the gallery. The rest of the styles will be primarily focused on the images.

Picture This: Styling the Images

The images will be squared by default to fit into two grid cells. Then, we will crop them to look like a rhombus (or rotated but without being turned).



.grid-gallery img {
  width: calc(var(--size) * 2);
  height: calc(var(--size) * 2);
  object-fit: cover;
  grid-column: auto / span 2;
  border-radius: 5px;
  clip-path: path("M90,10 C100,0 100,0 110,10 190,90 190,90 190,90 200,100 200,100 190,110 190,110 110,190 110,190 100,200 100,200 90,190 90,190 10,110 10,110 0,100 0,100 10,90Z"); 
}


Enter fullscreen mode Exit fullscreen mode

Notice that we are applying the styles to the image directly because that is how I initially structured the code for simplicity. If you go with a different HTML structure, like links with the pictures inside, you must adjust the code a little (but only a little).

Let's review the code one property at a time to check what it does:

  • width: calc(var( - size) * 2); Specifies the picture's width, which will be twice the size of a grid cell.
  • height: calc(var( - size) * 2); Sets the picture's height; it's the same value as the width, so we could have used something like aspect-ratio:1 instead.
  • object-fit: cover; We most likely changed the image's aspect ratio by setting a squared size. We indicate that we want the image to occupy the whole space to avoid stretching or gaps.
  • grid-column: auto / span 2; With this, we are indicating that the picture should go in the text available cell (auto) and occupy two cells' width (span 2).
  • border-radius: 5px; This is a decorative option: the photos will have small rounded corners as they look cleaner like that… but that's my non-designer opinion.
  • clip-path: path("…") We specify a path to crop the image in the rhombus shape with rounded corners. This approach has some shortfalls that we will review in a few paragraphs.

As you may have noticed, we don't specify in which cell each picture has to go. We add pictures and let the browser do its magic, positioning them correctly. There's no need to add complex formulas or verbose code for things that the browser will do automatically for you. Take advantage of those things!

About the clip-path: I used the path() method to create a rhombus shape and give it some rounded corners. But this approach has issues: it is only supported by some browsers, it is messy (too many points and curves, which will be a pain when we add transitions), and it is not responsive –why, in this day and age of the Internet, someone would release a non-responsive CSS feature beats me, but here we are.

Instead, we could use the polygon() method to simplify the parameters (to only 4 points) and add responsiveness and support to all browsers… but we would lose the rhombus rounded corners. Temani Afif shows a way to have rounded corners using the polygon() function, but that solution is not responsive.

The path() method will remain in this demo. But be aware that there are options that will make your code more well-supported.

Avoid overlapping

If you have tried the code we have so far, you will have noticed that pictures need to be arranged appropriately. Instead of rows of three and two photos interlaced, they are all rows of three elements.

Checking the gallery, the images are shifted in one column starting from the fourth and then every five: 4, 9, 14, 19, 24… That progression consists of multiples of five minus one.

Indicating that all the 5x-1 children will be shifted in one column (e.g., start in the second column instead of the first one) is relatively simple in CSS:



.grid-gallery img:nth-child(5n - 1) { 
  grid-column: 2 / span 2 
}


Enter fullscreen mode Exit fullscreen mode

As mentioned before, the browser will automatically place the new items in the next available cell to fit the element, so all we have to do is indicate this shift and the browser will do all the arranging work for us!

With that, we have the photo gallery with all the images in the right place, but what happens if we add more than eight pictures?

Adding more pictures

As mentioned above, the gallery is extendable: you can add/remove as many images as you want, and they will continuously adapt to this 3–2 grid pattern. Ideally, you will go with a multiple of five (5n, which would end the gallery in a row of two photos) or a multiple of five plus three (5n+3, which would end the gallery in a row of three images).

Version of the gallery with 15 photos

A 5n+3 photo number provides a more balanced distribution than a multiple of five… but that's my opinion. You can have as many pictures as you like.

Ready, Set, Action: Adding Interactions

So far, we only have a nice-looking image gallery, but it is static. We can do more to make it interactive, starting with what happens when the user places the mouse over the image.

Let's start by dimming the non-active pictures using the :has() pseudo-class. We want to select all the images that are not hovered when there's a hovered image in the gallery.



.grid-gallery:has(img:hover) img:not(:hover) {
  filter: brightness(0.5) contrast(0.5);
}


Enter fullscreen mode Exit fullscreen mode

Don't be scared of that selector; it is more straightforward than it seems. We read it from right to left: select all the non-hovered images from the .grid-gallery that has at least a hovered picture.

The following effect we will add is simple but elegant and pleasant: we expand the image (a little) and dim the non-hovered photos so that the person can focus on the "active" picture.



.grid-gallery img {
  /* ... */
  transition: clip-path 0.25s, filter 0.75s;
}

.grid-gallery img:hover {
  clip-path: path("M0,0 C0,0 200,0 200,0 200,0 200,100 200,100 200,100 200,200 200,200 200,200 100,200 100,200 100,200 100,200 0,200 0,200 0,100 0,100 0,100 0,100 0,100Z");
  transition: clip-path 0.25s, filter 0.25s;
  z-index: 1;
}


Enter fullscreen mode Exit fullscreen mode

On hover, we change the clip-path points, so they move from the rhombus points to the corners of a square (displaying the whole image as defined). It is a slight pain and something that could be considerably simplified by using polygon() instead of path().

We add a z-index to the hovered image so it will overlap all the sibling elements. The active photo may be behind later siblings if we don't include this code.

Finally, notice how we apply different transition times to the hovered image and the rest of the images. This is not only allowed but (almost) recommended to provide a more natural feeling: it feels robotic when everything happens simultaneously and at the same speed.

Focus and Outlines

If you opt for having other elements (e.g., links with the pictures inside) instead of using photos, you should also define the :focus interactions. They can be similar to the :hover one, but don't forget to modify the position of the outline!

Because we are using clip-path to shape the photo, the default browser's outline will likely fall outside the cropped area and be invisible, which is an accessibility violation.
To fix this issue, we can add a negative outline-offset value. That way, the outline will be within the cropped area and visible to the users. For example:



.grid-gallery a:focus {
  outline: 1px dashed black;
  outline-offset: -5px;
}


Enter fullscreen mode Exit fullscreen mode

We could add a pop-up showing a larger version of the image on click… but that falls outside this article's scope.

Conclusion

In this article, we've seen how to create an image gallery, and at the same time, we practiced with many CSS features:

  • Templating and aligning items with Grid
  • Custom properties (aka CSS variables)
  • Clipping with path() and polygon()
  • Transitions on user interaction
  • Aspect-ratios
  • Filters
  • Sibling selection with :has()
  • The object-fit property
  • User-triggered events like :hover or :focus

We also discussed alternatives and ways to achieve a similar result with broader browser support. I hope you enjoyed reading the article. To conclude, I'll leave here a demo of the photo gallery in action:

Top comments (18)

Collapse
 
francescovetere profile image
Francesco Vetere

This is awesome! 🤩
Particularly loved the clever use of :has(), it was really perfect in this situation and I cannot think of another CSS-only solution to obtain the same result without using it. Amazing!

Collapse
 
lexiebkm profile image
Alexander B.K.

Unfortunately, Firefox does not provide full-support to the :has pseudo-class. See
issue on Firefox

Collapse
 
auroratide profile image
Timothy Foster

What's great about this gallery solution, though, is that even without has it still works. I may not get the fancy greyscaling effect in Firefox, but it's nevertheless laid out nicely and still makes it obvious what image I'm highlighting.

A great example of progressive enhancement in action ( :

Thread Thread
 
francescovetere profile image
Francesco Vetere

Good point! 💪

Collapse
 
francescovetere profile image
Francesco Vetere

It seems that from version 121, Firefox will finally support it! 😉 caniuse.com/css-has

Collapse
 
tatu profile image
Vkad

Hey, love the idea. Was just wondering if I wanted to make each image a link how would go about and do this, because I tried this in VS code and it keeps overlapping the images.
Thank you in advance.

Collapse
 
alvaromontoro profile image
Alvaro Montoro

That happens because the CSS needs to be updated (just slightly). Here's a demo with links:

Collapse
 
tatu profile image
Vkad

Thank you so much!! This really helped a lot.

Thread Thread
 
tatu profile image
Vkad

I do have another inquiry if its not too much trouble. I was wondering what would I have to change in the CSS to make the hovered image appear larger but not overlap with the other images(So they would still be clickable).

Thread Thread
 
alvaromontoro profile image
Alvaro Montoro

I don't fully understand the request (they are still clickable.) Maybe on hover you could remove the new clip-path and add a scale() instead.

Thread Thread
 
tatu profile image
Vkad

Sorry if I was too confusing. I was just wondering how I could make the image bigger when hovering over it withour changing the initial size.

Collapse
 
oskargrosser profile image
Oskar Grosser

Nice effect! You should try out different transformations for revealing the full image, like rotating from rhombus to square, transforming from rhombus to circle to square, "unfolding" from rhombus to square, etc.

Speaking of the animation, I noticed that the transformation isn't symmetric: The top corners of the square come from the top corner of the rhombus (which looks fine), but the bottom-left and bottom-right corner of the square come from the bottom and right corner of the rhombus, respectively. This makes the transformation look off.

Collapse
 
ackvf profile image
Vítězslav Ackermann Ferko

Very nice effect.

Collapse
 
anitaolsen profile image
Anita Olsen

This is totally awesome, I LOVE IT! ✨

Collapse
 
russellbateman profile image
Russell Bateman

Way cool, friend!

Collapse
 
arthors01 profile image
arthorS01

Thank you so much for this!

Collapse
 
abrarali14 profile image
Abrar Ali

*Please make another thing *
❤❤❤❤❤

Collapse
 
genlyai_ profile image
Walter Santos

Many good tips! Thank you!