Justified Image Grid

In this article I am going to explain the justified image grid used for the photography on this site. In contrast to previous implementions that relied heavily on JavaScript this one is CSS only. Let’s start with the basic block: the image.

<img src="landscape.jpg">

To make this image fluid we set the width to 100% and the height to auto:

img {
  display: block;
  width: 100%;
  height: auto;
}

This works fine to stack images vertically (and these initial rules provide a nice fallback if Flexbox is not supported), but I wanted to show images in columns and rows. Additionally the height of the image can only be set once it is loaded, so on a slower connection this creates a jumpy behaviour. To avoid all of this we’ve added a bit more HTML:

<div class="grid">
  <div class="row">
    <div class="column">
      <img class="image" src="landscape.jpg">
    </div>
  </div>
</div>

The grid shows the image landscape.jpg in one column within one row. Here is the CSS:

.grid {
  position: relative;
  padding-bottom: 66.667%;
}

.row {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  display: flex;
  flex-direction: row;
}

.column {
  display: flex;
  flex-direction: column;
  flex-basis: 0;
}

.image {
  height: 0;
}

For the wrapping element grid we are setting the height to a percentage of its width – this is the padding-bottom hack. For this image the aspect ratio is 3:2, so its height should always be two thirds of its width. We calculate this value by dividing 100 through the aspect ratio: 100% ÷ 1.5 = 66.667%. By positioning the containing row to an absolute position and top/right/left/bottom to zero we make sure the row fills up all of the available area.

Each column within the row is laid out horizontally and stretches to fill the available width. And each image within a column is laid out vertically and stretches to fill the available height. The result looks exactly like before:

Next we are adding a square image next to the landscape image:

Most of the CSS is the same as in the previous example – I am going to add all the different values as inline styles (this is just for readability):

<div class="grid" style="padding-bottom: 40%">
  <div class="row">
    <div class="column" style="flex-grow: 1.5">
      <img class="image" style="flex-grow: 1" src="landscape.jpg">
    </div>
    <div class="column" style="flex-grow: 1">
      <img class="image" style="flex-grow: 1" src="square.jpg">
    </div>
  </div>
</div>

For the wrapping grid let’s just imagine these two images as one big image: the left one has an aspect ratio of 1.5 (3:2) and the right one of 1 (1:1), so combined it is 2.5. If the columns contain more than one image it isn’t that simple, so here is the full formula: for each column we sum up the inverted ratios of all the containing images (1 ÷ 1.5 = 0.667 and 1 ÷ 1 = 1), invert these values again (1 ÷ 0.667 = 1.5 and 1 ÷ 1 = 1), create the sum of them (1.5 + 1 = 2.5) and then invert this value again (1 ÷ 2.5 = 0.4). That’s how we end up at the 40% for the padding-bottom.

Now to the flex-grow values: for both images to have the same height the landscape image needs to be a bit wider than the square image. In percent it’s the aspect ratio of each column (which is the same as image in this case) divided by the sum of both aspect ratios: 100% × 1.5 ÷ 2.5 = 60% and 100% × 1 ÷ 2.5 = 40%. But instead of using these percentages we get the same result by using the aspect ratios as flex-grow values: 1.5 and 1. The images within each column should stretch over the full height, so for both we can take the flex-grow value 1.

Next we are adding another image, so we are having a landscape (3:2), a square (1:1) and a portait (2:3) image in one row:

<div class="grid" style="padding-bottom: 31.579%">
  <div class="row">
    <div class="column" style="flex-grow: 2.25">
      <img class="image" style="flex-grow: 1" src="landscape.jpg">
    </div>
    <div class="column" style="flex-grow: 1.5">
      <img class="image" style="flex-grow: 1" src="square.jpg">
    </div>
    <div class="column" style="flex-grow: 1">
      <img class="image" style="flex-grow: 1" src="portait.jpg">
    </div>
  </div>
</div>

The aspect ratios from left to right are 1.5, 1 and 0.667. For the padding-bottom we add sum these values up (3.167) and created the inverted value in percent: 31.579% (100% × 1 ÷ 3.167). To avoid having flex-grow values smaller than 1.0 (this causes display problems) we divide each flex-grow value by the smallest value of the row, so the values are 2.25, 1.5 and 1.

This is all the math needed for a single row of image. Next we are going to display the same three images in two columns: the left one with the landscape (3:2) and square (1:1) image stacked up vertically and the right one with the portrait image (2:3) by itself:

<div class="grid" style="padding-bottom: 78.947%">
  <div class="row">
    <div class="column" style="flex-grow: 1">
      <img class="image" style="flex-grow: 1" src="landscape.jpg">
      <img class="image" style="flex-grow: 1.5" src="square.jpg">
    </div>
    <div class="column" style="flex-grow: 1.111">
      <img class="image" style="flex-grow: 1" src="portrait.jpg">
    </div>
  </div>
</div>

padding-bottom is calculated according to the formula: first we sum up all inverted ratios for each column: 1 ÷ (1 + 0.667) + 1 ÷ 1.5 = 1.267. The inverted value in percent is 78.947%. The flex-grow value for the left side is 1 ÷ (1 + 0.667) and 1 ÷ 1.5 for the right side, so 0.6 and 0.667. Making sure the smallest value is 1 results in 1 for the left column and 1.111 for the right column. The flex-grow values for the images within a column so far have always been 1, but now we have two images within one column, one with the aspect ratio 1.5 and one with the aspect ratio 1. For flex-grow we again take the inverted values 0.667 and 1. We are again making sure the smalles value is 1.0, so it’s 1 and 1.5 – so the square image is 1.5 times the height of the landscape image.

I’ve mentioned before that inline styles were just used for readability. While the support for Flexbox is quite good I want to have a good fallback (I use @supports for this) – for me the grid layout is a progressive enhancement. Also on smaller screens having all images stacked up vertically might be a better option (and media queries are not possible with inline styles). So for all the flex-grow and padding-bottom values I am creating separate classes, simplified it looks like this:

<div class="grid padding-bottom-bc3b09">
  <div class="row">
    <div class="column flex-grow-e95f50">
      <img class="image flex-grow-e95f50" src="landscape.jpg">
      <img class="image flex-grow-c37c5b" src="square.jpg">
    </div>
    <div class="column flex-grow-e3b447">
      <img class="image flex-grow-e95f50" src="portrait.jpg">
    </div>
  </div>
</div>

And this can then be wrapped in a media query and a @supports rule:

@media screen and (min-width: 600px)
{
  @supports (display: flex)
  {
    .grid {
      position: relative;
      padding-bottom: 66.667%;
    }

    .row {
      position: absolute;
      top: 0;
      right: 0;
      bottom: 0;
      left: 0;
      display: flex;
      flex-direction: row;
    }

    .column {
      display: flex;
      flex-direction: column;
      flex-basis: 0;
    }

    .image {
      height: 0;
    }

    .padding-bottom-bc3b09 {
      padding-bottom: 78.947%;
    }

    .flex-grow-e95f50 {
      flex-grow: 1;
    }

    .flex-grow-e3b447 {
      flex-grow: 1.111;
    }

    .flex-grow-c37c5b {
      flex-grow: 1.5;
    }
  }
}

So if the screen size is smaller than 600px or Flexbox is not supported all the images are stacked vertically. Which is a nice fallback.

The Future with CSS Grid Layouts

I am excited about CSS Grid Layout, which will simplify this code even more:

<div class="grid padding-bottom-bc3b09">
  <div class="row grid-template-columns-f0cbde">
    <div class="column grid-template-rows-e0b3ea">
      <img src="landscape.jpg">
      <img src="square.jpg">
    </div>
    <div class="column grid-template-rows-bde722">
      <img src="portrait.jpg">
    </div>
  </div>
</div>

CSS:

@supports (display: grid)
{
  .grid {
    position: relative;
    padding-bottom: 66.667%;
  }

  .row {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    display: grid;
  }

  .column {
    display: grid;
  }

  .grid-template-columns-f0cbde
  {
    grid-template-columns: 1fr 1.111fr;
  }

  .grid-template-rows-e0b3ea
  {
    grid-template-rows: 1fr 1.5fr;
  }

  .grid-template-rows-bde722
  {
    grid-template-rows: 1fr;
  }
}

Also with this approach the fallback would be that all the images are stacked up vertically.

Using Different Aspect Ratios

In all the examples we’ve used the aspect ratios of the images, so 1.5 for landscape, 1 for square and 0.667 for portrait images. But one could of course use completely different ones or the same one for all images (for example to make all tiles square). First this would result in the images being stretched, but we can avoid that by using object-fit: cover for the image. If you scroll up again there are two declarations I haven’t explaind yet: flex-basis: 0 for column and height: 0 for image. The first keeps the main size of all columns the same and the second makes sure the image doesn’t size past the bounding box.