Designing Your Own Masonry Grid Layout From Scratch

I was fascinated by the masonry layout made popular by Desandro famous jQuery plugin the moment I saw it. Beyond the fascination I was intrigued at how was it done, what was going on under the hood? It's no hazard that the first official post of this hacker blog takes a deep dive in creating our very own version of it.

design your own masonry grid layout

Goals and Challenges

First let's outline some of the goals that we are trying to achieve by the end of the project:

1- Grid layout with nothing but gutter gaps between rows and columns
2- Blocks should tile nicely no matter the difference in heights
3- Elements order has to be preserved from left to right and not vertically like css3 columns
4- Images loading should be accounted for and not break the layout
5- Would be nice if it was cross browser
6- Videos if added have to fit nicely and display at a pleasant aspect ratio

Alright let's dive in

The basic markup

    <div class="container" class="container">
        <!--- put blocks here --->
    </div>

Blocks will follow the structure below

    <div class="blocks">
        <div class="box">
            <h3>Some title 1</h3>
            <p>Some text...</p>
        </div>
    </div><div class="blocks">
        <h3>Some title 2</h3>
        <!--- add image --->
        <img src="http://lorempixel.com/600/900?p=4" />
        <p>Some other text...</p>
    </div>
    <!--- create 10 more blocks randomly add image on some --->

We start with container div that has our blocks each containing a box. The excellent lorempixel.com service is used to randomize images and force the initial script to deal with unspecified picture measurements.
The most important part though is that there is no space between the closing div of each block and the opening div of the next one

    <!--- no gap between closing and opening elements --->
    <!--- that use the display inline block rule --->
    </div><div class="blocks">

I got to find out after endless hours that the blank space will translate into the browser and break the layout. Chris Coyier explains it well and offers many solutions in his extensive article Fighting The Space Between Inline Block Elements. I highly recommend that you read it and take note of the different solutions outlined there.

The CSS

I originally started with floated elements but found it to be a mess to deal with when elements had differents heights. I decided to go with inline-block instead which besides big quirk - mentioned above - offered more control for the layout.

The rules are pretty minimal.

* {  
    margin: 0;
    padding: 0;
    box-sizing: border-box;
  }
  html, body {
    width: 100%;
    height: 100%;
  }
  html {
    font-size: 100%;
    font-family: "Roboto Slab", serif;
    height: 100%;
  }
  body {
    line-height: 25px;
    height: 100%;
    background-color: #e9e9e9;
  }
  img {
      max-width: 100%;
  }
  #container {
    margin: 10px;
    position: relative;
    height: 100%;
    opacity: 0;
    /*** skipping vendor prefixes ***/
    transition: all 2s linear;
  }
  .blocks {
    display: inline-block;
    vertical-align: top;
    width: 25%; 
  }
  .blocks > .box {
    margin: 5px;
    border: 1px solid #dedede;
    padding: 15px;
    vertical-align: top;
    background-color: #fff;
    color: #333;
  }

The #container is given a position relative since it is going to house our absolutely positionned block elements. The .blocks class has the display: inline-block rule and are aligned vertically to the top. The width of 25% is arbitrary. In the javascript we will override the width of each element to be equal to the first one in the set.

For the .box class the rule to take note of is the margin

margin: 5px; /* half of the parent container margin */

which is half of the container margin rule - this creates very balanced gutters between the block

The Javascript

Now that we have our initial setup in place let's add the magic sauce via Javascript. First let's break down our logic in plain English:

  1. Find out how wide the parent container is
  2. Find out how many elements we can lay across
    • We will calculate the width of the first element and apply it to all
    • Later we might want to add it as a config parameter
  3. Get the coordinates of each block and get the height
  4. Group blocks by sets that share the same top position
  5. Calculate the adjusted top position of each element in relation to height of the one directly above it

I opted to use Lodash as my utility belt and to do some of the heavy lifting. I also found out about the super awesome getBoundingClientRect() function that had high praises by John Resig - I know it was in 2008 but still holds value for me.

The initial code looked like:

(function (window) {  
    window.addEventListener('load', function(event) {
        var container = document.getElementById('container'), 
        blocks = [].slice.call(document.querySelectorAll('.blocks')), 
        boxes = [], // all blocks with their coordinates
        count = 0, 
        bricks, // grouped set by top positions
        indexes, // groups all top positions key names
        avWidth, // available width
        colWidth, // width of 1 column
        maxCols; // maximum number of columns per row
    });
})(window);

Now that we have our variables in place let's start by getting all the blocks into one array

blocks.forEach(function (i) {  
      /* using getBoundingClientRect()
       * box will have the bottom, height, left, right, top, width
       * values of the elemnt
       */
      var box = i.getBoundingClientRect();
      box['topPos'] = 0; // adding to track top position
      box['index'] = count; // adding index to reference proper DOM element
      boxes.push(box);
      count++;
  });

Next we can now populate the remaining variables we had earlier since they all depend on the blocks set:

bricks = _.groupBy(boxes, 'top');  
indexes = _.keys(bricks);  
avWidth = container.getBoundingClientRect().width;  
colWidth = Math.floor(blocks[0].getBoundingClientRect().width);  
maxCols = Math.floor(avWidth / colWidth);

Finally we can get to our last step where we calculate the appropriate positions for our blocks

for(var i = 0, j = indexes.length; i < j; i++) {  
    (function(i) {
    var  pSet = bricks[indexes[i - 1]],
    thisSet = bricks[indexes[i]],
    topPos = 0; 

    _(thisSet).forEach( function (b, x) {
    var cssString = " position: absolute;";
    cssString += " left: "+(colWidth * x)+"px;";
    if(pSet) {
    topPos = parseInt(pSet[x]['topPos']);
    b['topPos'] = topPos + b['height'];
    }
    else {
    b['topPos'] = b['height'];
    }

    cssString += " top: "+topPos+"px;";
    blocks[b['index']].style.cssText += cssString;
    });
    })(i);
  }

We are looping through the indexes array that contains the key for each of the sets inside of the bricks object. We declare a pSet variable that will hold the previous set if it exists and we add a local topPos variable with a value of 0.

var pSet = bricks[indexes[i - 1]],  
    thisSet = bricks[indexes[i]],
    topPos = 0;

Now looping through the current set we add a cssString variable that holds some additional rules for each block. The we calculate the top position of the element by looking at the previous set - if not it defaults to 0.

_(thisSet).forEach( function (b, x) {  
        var cssString = " position: absolute;";
            cssString += " left: "+(colWidth * x)+"px;";
        if(pSet) {
            topPos = parseInt(pSet[x]['topPos']);
            b['topPos'] = topPos + b['height'];
        }
        else {
            b['topPos'] = b['height'];
        }
        .....

Then we add the new top position to the element

....  
        cssString += " top: "+topPos+"px;";
        blocks[b['index']].style.cssText += cssString;
    });

And that's it!!! No not quite. For the crazy Math that's about it - presentation wise I thought it was best to have the container with an initial opacity of 0 to start with in the CSS and turn it back on with the Javascript once the layout was ready for view hence the last line.

container.style.opacity = "1";

At this point I was in coding heaven, when I refreshed my browser with 12 blocks on my widescreen everything looked perfect!! Safari, Chrome and Firefox were rendering as expected and even IE 9 looked great.

Adding video

The window.load event seems to handle images pretty nicely. Now let's add a video to the mix. The videos need to be contained within their parent block and most important keep their aspect ratio. I initially thought of using Fitvid.js but a CSS only solution - no jQuery dependency - was way more attractive. I found this excellent solution from this post Avex Designs - Responsive Youtube Embed

This is the video markup

<div class="video">  
    <iframe width="560" height="315" src="//www.youtube.com/embed/b35zsIbGLsM" frameborder="0" allowfullscreen></iframe>
</div>  

an the css to take care of it

.video {  
    position: relative;
    padding-bottom: 56.25%; /* (9 / 16) x 100 */
    padding-top: 30px; height: 0; overflow: hidden;
}

.video iframe,
.video object,
.video embed {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
}

Even though the solution is very youtube focused it was perfect for what I was after.

At this stage I was very pleased with the initial result until I decided to add more elements and check how it will handle invisible blocks.

Let's just say it did not go well. Every element that was not visible initially - below the fold, was stacking against the lowest blocks on the viewport.

After logging out the bricks object I found out that everything below the fold was returning a negative top position which was a result of how getBoundingClientRect() was handling those cases.

After more research on the subject I ran into a great explanation of the issue by Nicolas Zakas in his book Professional Javascript for Web Developers.

So I adapted (read copied shamelessly) his function at the top of my script.
The updated script now looks like this:

/**  
  * getBoundingClientRect()
  * helper function picked from 'Pro Javascript for Web Developers ' 
  * by Nicolas Zakas
  **/
function getBoundingClientRect(element){  
  var scrollTop = document.documentElement.scrollTop;
  var scrollLeft = document.documentElement.scrollLeft;
  if (element.getBoundingClientRect){
    if (typeof arguments.callee.offset != "number"){
      var temp = document.createElement("div");
      temp.style.cssText = "position:absolute;left:0;top:0;";
      document.body.appendChild(temp);
      arguments.callee.offset = -temp.getBoundingClientRect().top -
      scrollTop;
      document.body.removeChild(temp);
      temp = null;
    }
    var rect = element.getBoundingClientRect();
    var offset = arguments.callee.offset;
    return {
      left: rect.left + offset,
      right: rect.right + offset,
      top: rect.top + offset,
      bottom: rect.bottom + offset
    };
  } else {
      var actualLeft = getElementLeft(element);
      var actualTop = getElementTop(element);
    return {
      left: actualLeft - scrollLeft,
      right: actualLeft + element.offsetWidth - scrollLeft,
      top: actualTop - scrollTop,
      bottom: actualTop + element.offsetHeight - scrollTop
    };
  }
}

window.addEventListener('load', function(event) {  
'use strict';  
.....

You can get enlightened by the master himself in this blog post Getting Element Dimensions - Nicolas Zakas

With that new powerful function in place I just needed to adjust my blocks set logic

blocks.forEach(function (i) {  
  /*
  Replace:
  var box = i.getBoundingClientRect();
  with:
  */
  var box = getBoundingClientRect(i);
  box['topPos'] = 0;
  // calculate and add height
  box['height'] = box['bottom'] - box['top'];
  box['index'] = count;
  boxes.push(box);
  count++;
});

This time around it really worked, tiling perfectly and taking into account below the fold elements. Just awesome.

Closing thoughts

This was a very enjoyable hack and it got me to appreciate even more the open source community and what gems people like David Desandro are making available at no cost.

This is more intended as a learning project, what I wanted to share was one way of solving the problem. Performance wise it is not quite ready for a production website and it does not include many of the features that Masonry or Wookmark have to offer such as:

  • ImagesLoaded
  • Window Resize
  • Different width blocks
  • Responsiveness
  • and many more....

Those will eventually get added. I the meantime you can check the working demo here Blockery and take a look at the source code here Blockery on github

At this present stage this works well on Chrome, Safari, Firefox and even IE 9. It should not take much to adapt it forIE8, but that will be for another iteration.

I hope you found this valuable and useful. Please share your tips, suggestions and more important let me know if you want to contribute and add to the project.

Ady Ngom

Ady Ngom

http://adyngom.com

Ady Ngom is a freelance web and mobile application developer who has a passion for well crafted interfaces. You might catch him humming a good tune while taking long walks with a camera in hands.

View Comments
Navigation