For some time I have been attempting to recreate “masonry” effects in flexbox, 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?
The solution – also available on CodePen – 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
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:
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, due to a browser bug) 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.
Explore the code for Modern Masonry on CodePen
Pro CSS3 Animation, Apress, 2013
Massive Head Canon
The New Defaults
CSSslidy