February 6, 2021

Image Gallery with CSS Grid

I built an image gallery with CSS grid and a lightbox. CSS Grid is great fun.

I've been out-of-the-loop on CSS Grid since...well forever. I read about it and tried to learn it when it first got released and implementation started happening, but I wasn't able to get a grasp. I had a lot of other things going on and I didn't need to use it. Afterall, I was already using flexbox on everything. It was difficult for me to imagine the possibilities of grid because I wasn't getting the same thing from the primers that everyone else was and because I didn't try to use it (probably because I didn't get it).

In the past month, I've been rebuilding my website, and one of the big pieces is the blogroll. It's always been kinda basic, kinda boxy, not a whole lot of "design" happening there - just a clickable list of my posts, maybe with some images, and sometimes sortable/searchable. I wanted this list to still be basic, but look a little better while maintaining functionality. I started by just putting each element in a column, which in the Bulma framework, is just how horizontal layouts are built. Columns collapse at breakpoints of 1024px & 768px, eventually collapsing down in to a single column.

I wanted to find a way to highlight the most recent post and juse make things more interesting, so I started playing around with grid. But first, I had to learn it. I spent some time on Learn CSS Grid and got a really good start. I immediately understood the advantages of using grid for layout vs. using flexbox and whatever the browser decides. The control that grid provides is great and allows for creative layouts at multiple breakpoints.

I was able to implement grid on my blogroll, and I'm super happy with the result. After building out that portion of my site, I "got" grid and thought up this cool use case: a Pinterest-style image gallery with a built-in lightbox.

The markup is my favorite part of this. It's very straightforward. A container, the child items, and the lightbox markup (which is actually just a Bulma modal).

<!--the grid-->
<div class='collection'>
  <div class='item'>
    <img src='https://source.unsplash.com/featured/?fashion' />
  ...a bunch more div.items...

<!--the lightbox-->
<div class='modal' id='lightbox'>
  <div class='modal-background'></div>
  <div class='modal-content'><img id='lb_img' /></div>
  <button class='modal-close'></button>

Setting Up The Grid

For the display, we need to start by defining the grid columns and rows. To do that, we use the grid-template-columns property and the repeat() function, which accepts the number of times the defined tracks should repeat and the track definition. The track definition is set using the minmax() function, which accepts the minimum size of the track and the maximum size. That sounds like a bunch of stuff, but basically, we're telling the grid: "Hey. You're a grid. You have X columns, each with a minumum width of Y and a maximum width of Z.". In this case, the "X" in the above statement is auto-fit, which calculates the amount of columns based on available space. "Z" is 1fr, or 1 fraction of the available space.
I defined the rows arbitrarily at 100px. This just "felt" right. I also used grid-auto-rows, which explicitly states the sizes of implicit rows. I defined the gap at .5em, and the grid-auto-flow as "dense", which packs the automatically generated grid positions in there real good so there is no "blank spaces". I threw a media query in there, too, to make the gap a wee bit smaller on smaller screens.

.collection {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  grid-template-rows: 100px;
  grid-gap: .5em;
  grid-auto-rows: 100px;
  grid-auto-flow: dense;

Styling "random-ish" Items

The items can be styled however you like, but I used flexbox to center the images in the item container. The above code for the grid definition will get you going nicely, but to change up some sizes and have it look like many of the items are of different sizes, the grid-column and grid-row properties need to be messed with.
By using the :nth-of-type() selector, we can select "random-ish" items to manipulate. I say "random-ish" because there won't be a discernable pattern with the auto-flow being dense - it will seem random to the end user. I used two definitions here, but you can play with one or three or seven.
The grid-column property is shorthand for grid-column-start and grid-column-end, which define which column an item should start at and where it should end. Using the span keyword as the property tells the grid that we don't know where this item will start, but we want it to span X columns, where X is the number of columns we want that item to span. So, no matter if the natural order puts this item at grid-column 3 or 1 or 88, the column end will be +2 of that number. The grid-row property is the same concept, just on the Y axis

.item:nth-of-type(3n) {
  grid-column: span 2;
.item:nth-of-type(4n) {
  grid-row: span 2;

Aside from the design, that is the meat and potatoes. Add in some media queries to break it down when we don't have 2 columns to span and we're good to go:

@media (max-width: 400px) {
  .item:nth-of-type(3n) {
    grid-column: span 1;

The JavaScript to pull it together

All that's left is the JavaScript to get the lightbox going. The items are hoverable and clickable - so we just need to listen for that click. Once the click happens, find the image source and apply it to the lightbox modal image. Oh, and don't forget to add the class "is-active" to the lightbox modal.
The click event on the lightbox closing button needs to be listened for, as well. Once that gets clicked, just remove the "is-active" class from the lightbox modal. Boom!

var images = document.querySelectorAll('img');
const lb = document.getElementById('lightbox');
const lb_img = document.getElementById('lightbox_img');
if (images) {
  images.forEach(el => {
    el.addEventListener('click', () => {
      lb_img.src = el.getAttribute('src');
var close = document.querySelector('.modal-close');
if (close) {
  close.addEventListener('click', () => {

I put it all together in this nice little CodePen for ya. Oh! This gives me an opportunity to talk about something else: the images I'm using come from Unsplash Source, which is a real cool way to get great stock photos (no sponsorship here, I just really love Unsplash). I've used Unsplash for years and just found out that this is a thing!

See the Pen Grid Gallery w/ Lightbox by Nate Northway (@the_Northway) on CodePen.

Here's some links for you: