For some time I have been attempting to recreate “masonry” effects in , where images are arranged like bricks in a wall. My previous attempt was moderately successful, but it ran ragged and lacked the dynamism I wanted.

Then, after working on the recent “Random Images With Flexbox and JavaScript” article, I had an epiphany: why not use JavaScript to read the image’s aspect ratios, and use that to determine the correct flex value for each element?

This solution allows designers to load images of any dimension and aspect ratio into a container element, apply a class, and have a seamless image masonry effect generated automatically on the page using modern web standards, with no plugins or frameworks required.

The Loading Challenge

Photograph of a butterfly on a purple flower Photograph of a butterfly
The script automatically rescales images of different sizes and aspect ratios to fit perfectly together

The first challenge is that an image must be completely loaded onto a page before JavaScript can determine anything about it: just having the <img> tag present is not enough. Historically, there are three main ways of dealing with this:

  1. Try to detect that every element is loaded before running any more JavaScript.
  2. Load the images using JavaScript itself.
  3. Abandon JavaScript entirely, and determine the height and width of the images server-side using PHP or a similar technology.

For the purposes of illustration I’ll use the first two techniques, although it should be noted that the second runs counter to the principles of progressive enhancement.

I’ll start with an empty <div>. The images to be inserted inside the <div> will have <figure> elements wrapped around them, so I’ll set up the styles for the expected DOM content:

* { 
	box-sizing: border-box; 
}
.quantize {
	display: flex;
	flex-flow: row wrap;
	font-size: 0;
	width: 80%;
	margin: 0 auto; 
}
.quantize figure {
	margin: 0; 
}
.quantize figure img {
	width: 100%;
	height: auto; 
}

Then the first part of the JavaScript:

var container = document.getElementsByClassName('quantize')[0];
var butterflies = [ "orange-butterfly.jpg", "butterfly-on-yellow-flower.jpg", "butterfly-on-petal.jpg", "albino-butterfly.jpg", "blue-butterfly.jpg"];
function preloadImage(filename){
	var img=new Image();
	img.onload = function(){
		img.aspectRatio = img.naturalWidth / img.naturalHeight;
		var fig = document.createElement('figure');
		fig.appendChild(img);
		container.appendChild(fig);
	};
img.src= filename;
img.alt = "";
}
function loadImages() {
	for (var i = 0; i < butterflies.length; ++i) { 
		var filename = butterflies[i];
		preloadImage(filename);
		}
	}

The first two lines of code identify the element into which the images will be inserted, and lists the images I want to load as an array. The preloadImage function will be fed the filenames from the array and create new image elements in the DOM from that information. Within that lies an onload method for each image, which creates a new property representing the ratio between the image’s natural width and height. With this information added to the image, the function wraps the image in a <figure> element and adds it inside the container. Note that the order is important: img.onload must be placed before the image source is assigned.

Next, we need a function that sorts the images by their aspect ratios, i.e. the number that was calculated earlier. I’ve created a fitFlex function to do just that:

function fitFlex() {
	var flexGroup = container.querySelectorAll("figure");
	var flexArray = Array.prototype.slice.call(flexGroup, 0);
	flexArray.sort(function (a, b) { 
		imageAspectRatioA = a.firstElementChild.aspectRatio;
		imageAspectRatioB = b.firstElementChild.aspectRatio;
		if (imageAspectRatioA < imageAspectRatioB) { return 1; }  
		if (imageAspectRatioA > imageAspectRatioB) { return -1; }
		return 0;
	});
	var widest = flexArray[0].firstElementChild.aspectRatio;
	var smallestWidth = "300";
	flexArray.forEach(function(box) { 
		var flex = 1 / (widest / box.firstElementChild.aspectRatio);
		if (flex == 0) { flex = 1; }
		boxWidth = smallestWidth * flex;
		box.style.cssText = "flex: "+flex+"; min-width: "+boxWidth+"px;
		}); 
}

In brief, this code grabs all the created <figure> elements and places them into an array, sorting them so that the figure with the widest image appears first. (Note that this does not change the order in which the images actually appear on the page).

This element will be given a flex value of 1 and the min-width decided in the code, with every other image provided with a flex value and min-width relative to that. For example, the generated code for the first image in the series looks like this:

<figure style="flex: 0.938 1 0px; min-width: 281.425px;"><img src="orange-butterfly.jpg" alt></figure>

The result means that every image, no matter what its height and width, will fit neatly into a grid, once the two functions are called:

loadImages();
window.addEventListener("load", function() {
	fitFlex();
});

I’m very pleased with the result (although it does have layout problems in Firefox at small viewport sizes) and intend to publish the script on GitHub with more options in the very near future.

Images by Tony Hisgett, plancas67, Alain Picard, Bill Gracey and Peter Weemeeuw, licensed under Creative Commons.

Enjoy this piece? I invite you to follow me at twitter.com/dudleystorey to learn more.
Check out the CodePen demo for this article at https://codepen.io/dudleystorey/pen/KreAx