Two Column masonry layout in vanilla javascript

In a recent project I had the need to render a two column layout where the articles filled up all available space and stacked left to right.

This is basically a masonry layout, but masonry has to fill top to bottom rather than left to right. I wanted it to fill left to right.

So, I implemented a solution with css floats and :nth-child(odd) and :nth-child(even) selectors, but this comes up against a problem when one of the columns has two short articles and the other side has a long one. The layout breaks! 🙁

I researched using css columns for the layout but this suffers from a few issues.

The only remaining option was to add a little bit of javascript to help out.

I found this great very simple jquery script but I didn’t want to use jquery, and it can be achieved very simply without it. So i rewrote it in vanilla javascript.

The main algorithm in ES2015 looks like this:

let leftColumnHeight = 0, rightColumnHeight = 0
let articles = document.querySelectorAll('.articles')
articles.forEach((el) => {
    let outerHeight = el.outerHeight;
    if (leftColumnHeight > rightColumnHeight) {
        el.classList.add('right')
        rightColumnHeight += outerHeight
    } else {
        leftColumnHeight += outerHeight
    }
})

This runs through all the articles in your column and calculates the height of the article, if the right column total height is smaller then add a class that sets the article on the right.

The companion css to implement the layout is easy:

.article {
    width: 50%;
    float: left;
    clear: left;
 
.article.right {
    float: right;
    clear: right;
}

Of course, this isn’t the whole story. If the screen is resized then column layout might no longer be correct, so I added a debounced listener on window.resize event. Also the el.outerHeight property doesn’t include margins, so we need to add an extra function for that.

You can find the full gist here or find it embedded below.

/**
* A simple vanilla javascript class to implement a two column, left-to-right layout
*
* @author Tim Ross <timrross@gmail.com>
*/
/**
* A class that implements the two column layout.
* Called like this:
* <code>
* let layout = new TwoColumnLayout('.article)
* </code>
*/
class TwoColumnLayout {
constructor(selector, rightClassName = 'right') {
this.selector = selector
this.rightClassName = rightClassName
this.debounceLayout = debounce(() => {
this.layout()
}, 100)
this.layout()
this.addEventListeners()
}
/**
* Add the class 'right'
*/
layout() {
let leftColumnHeight = 0, rightColumnHeight = 0
let articles = document.querySelectorAll(this.selector)
articles.forEach((el, index) => {
el.classList.remove(this.rightClassName)
let outerHeight = this.calculateOuterHeight(el);
if (leftColumnHeight > rightColumnHeight) {
el.classList.add(this.rightClassName)
rightColumnHeight += outerHeight
} else {
leftColumnHeight += outerHeight
}
})
}
addEventListeners() {
window.addEventListener(
"resize",
() => {
this.debounceLayout();
},
false
);
}
/**
* Include margins in the outerheight calculation
* @param {Element} el
*/
calculateOuterHeight(el) {
var height = el.offsetHeight;
var style = getComputedStyle(el);
height += parseInt(style.marginTop) + parseInt(style.marginBottom);
return height;
}
}
function debounce(func, wait, immediate) {
var timeout
return function () {
var context = this,
args = arguments
var later = function () {
timeout = null
if (!immediate) func.apply(context, args)
};
var callNow = immediate && !timeout
clearTimeout(timeout)
timeout = setTimeout(later, wait)
if (callNow) func.apply(context, args)
}
}
export default TwoColumnLayout
.article {
width: 50%;
float: left;
clear: left;
.article.right {
float: right;
clear: right;
}

To use the class you can just instanciated it with the correct selector like this:

import TwoColumnLayout from './two-column-layout.js'
let layout = new TwoColumnLayout('.article');
view raw usage.js hosted with ❤ by GitHub

Hope it helps someone.

Leave a Reply

Your email address will not be published. Required fields are marked *