Another Justified Image Grid
Since I wrote this article on how display a justified image grid I’ve refactored the code. Instead of having one block for each row I wanted to pass a nested array of rows. Each row could contain one or more columns and each column could contain one or more images.
One Row With One Column
But let’s start with the smallest building block: the image.
The HTML:
<div class="asset" style="padding-bottom: 66.667%;">
<img class="asset-image" src="landscape.jpg">
</div>
The CSS:
.asset {
position: relative;
}
.asset-image {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
}
The image tag is wrapped in an asset element with the height set to a percentage of its width – this is the padding-bottom hack. The aspect ratio of this image is 3:2, so its height is two thirds of its width. The padding value is calculated by dividing 100 through the aspect ratio: 100% ÷ 1.5 = 66.667%. By positioning the containing asset-image to an absolute position, top/left to zero and width/height to 100% the image fills up all of the available space.
One Row With Two Columns
This code works fine to stack images vertically (just adding asset after asset), but I also wanted to show images in columns. Here I am adding a square image next to the landscape image (so there’s one row with two columns):
The HTML:
<div class="flex flex-row">
<div style="flex: 1 0 60%;">
<div class="asset" style="padding-bottom: 66.667%;">
<img class="asset-image" src="landscape.jpg">
</div>
</div>
<div style="flex: 1 0 40%;">
<div class="asset" style="padding-bottom: 100.0%;">
<img class="asset-image" src="square.jpg">
</div>
</div>
</div>
I’ve added several divs to this code block: the wrapping div is for the row and each column is an own div containing the asset. I’ve added two utility classes to the container: flex and flex-row – the first sets the display to flex, the second sets the direction to rows. The aspect ratios are 1.5 (3:2) for the left and 1 (1:1) for the right image – the sum is 2.5. The property flex is a shorthand for flex-grow, flex-shrink, and flex-basis. The first two values are always set to one and zero, the third is calculated. For both images to have the same height the landscape image needs to be a bit wider than the square image. In percent this is the aspect ratio of each column divided by the sum of both aspect ratios: 100% × 1.5 ÷ 2.5 = 60% and 100% × 1 ÷ 2.5 = 40%.
One Row With Two Columns and More Images
This calculation works out fine for adding columns containing one image. But let’s create another layout with two columns: the left column contains a landscape and square image stacked vertically and the right column contains a single portrait image:
The HTML:
<div class="flex flex-row">
<div class="flex flex-column" style="flex: 1 0 47.368%;">
<div style="flex: 1 0 40%;">
<div class="asset" style="padding-bottom: 66.667%;">
<img class="asset-image" src="landscape.jpg">
</div>
</div>
<div style="flex: 1 0 60%;">
<div class="asset" style="padding-bottom: 100%;">
<img class="asset-image" src="square.jpg">
</div>
</div>
</div>
<div style="flex: 1 0 52.632%;">
<div class="asset" style="padding-bottom: 150%;">
<img class="asset-image" src="portrait.jpg">
</div>
</div>
</div>
The calculation gets a bit more complicated, but let’s go through it step by step: the landscape image has an aspect ratio of 1.5 (3:2), the square image of 1 (1:1) and the portrait image of 0.667 (2:3). The previously mentioned formula of just adding up the aspect ratios only works for single images, but here’s a more general one: sum up the inverted aspect ratios of each column and invert that value again. So for the left column this would be 1 ÷ 1.5 = 0.667 and 1 ÷ 1 = 1, so in sum 1.667. Inverting this again gives us the value 0.6. For the right side it’s 1 ÷ 0.667 = 1.5 and this one’s inverted again gives us 0.667. The aspect ratio of the left column divided by the sum of these two values is 100% × 0.6 ÷ 1.267 = 47.368%. And for the right column the value is 100% × 0.667 ÷ 1.267 = 52.632% (in sum again 100%).
I’ve added two more utility classes to the left column: flex and flex-column so the containing items will be aligned vertically. For each row within this column we again calculate the flex-basis value by taking the inverted aspect ratios (so 0.667 and 1.0) divided by sum: so that’s 100% × 0.667 ÷ 1.667 = 40% and 100% × 1.0 ÷ 1.667 = 60%. As the right column contains only one image we don’t have to do this and just add this element as it is.
Multiple Rows
As a last example I am adding another row to that layout. And I am also explaining some other classes that need to be added. First let’s take a look at the layout:
The HTML:
<div class="flex flex-row flex-wrap justify-end overflow-hidden">
<div class="flex flex-column min-height-100p" style="flex: 1 0 47.368%;">
<div style="flex: 1 0 40%;">
<div class="asset min-height-100p" style="padding-bottom: 66.667%;">
<img class="asset-image" src="landscape.jpg">
</div>
</div>
<div style="flex: 1 0 60%;">
<div class="asset min-height-100p" style="padding-bottom: 100%;">
<img class="asset-image" src="square.jpg">
</div>
</div>
</div>
<div class="min-height-100p" style="flex: 1 0 52.632%;">
<div class="asset min-height-100p" style="padding-bottom: 150%;">
<img class="asset-image" src="portrait.jpg">
</div>
</div>
<div class="min-height-100p" style="flex: 1 0 21.053%;">
<div class="asset min-height-100p" style="padding-bottom: 150%;">
<img class="asset-image" src="portrait.jpg">
</div>
</div>
<div class="min-height-100p" style="flex: 1 0 47.368%;">
<div class="asset min-height-100p" style="padding-bottom: 66.667%;">
<img class="asset-image" src="landscape.jpg">
</div>
</div>
<div class="min-height-100p" style="flex: 1 0 31.579%;">
<div class="asset min-height-100p" style="padding-bottom: 100.0%;">
<img class="asset-image" src="square.jpg">
</div>
</div>
</div>
Using flex-basis for the width wraps an item to the next row as soon as the previous items have reached 100%. In this example the first two columns sum up to 100% (47.368% + 52.632%). The next three columns again sum up to 100% (21.053% + 47.368% + 31.579%). To make the columns wrap we just add the utility class flex-wrap. The other two classes you didn’t see before are justify-end and overflow-hidden – these two are a bit more esoteric: in some cases I’ve seen gaps in the layout (maybe caused by rounding errors). Adding this code takes care of that. And for each column and asset I am adding the class min-height-100p, which sets the min-height to 100%, this fills up any vertical gaps. Here are all utility classes used:
.flex {
display: flex;
}
.flex-row {
flex-direction: row;
}
.flex-column {
flex-direction: column;
}
.flex-wrap {
flex-wrap: wrap;
}
.justify-end {
justify-content: flex-end;
}
.overflow-hidden {
overflow: hidden;
}
.min-height-100p {
min-height: 100%;
}
For this site I’m setting the layout manually, but I’ve already experimented with using a linear partitioning algorithm to distribute a list of images more or less evenly over a certain amount of rows.
Adding Gaps
One more thing I wanted to add: gaps.
To make this work I’ve shrunk the asset-image a tiny bit by adding half of the gap to the top/left and subtracting it from the width/height. These values then add up to the full gap when the edges of a photo touch. But there’s also half the gap outside of the full grid. To compensate this I am adding half of the gap as negative margin to the grid. Here’s the HTML:
<div class="overflow-hidden">
<div class="grid flex flex-row flex-wrap justify-end overflow-hidden">
…
</div>
</div>
Here is the CSS:
.grid
{
margin: calc(-2px);
}
.grid .asset-image
{
width: calc(100% - 4px);
height: calc(100% - 4px);
left: calc(2px);
top: calc(2px);
}
This would also be a good use case for custom properties (so each grid can have its own gap value). I am not totally happy with this solution as the aspect ratio of the image is changed (by a tiny bit) – but well, whatever works. One workaround to this would be to use the object-fit value cover – in this case the images are cropped, but not distorted.