Doing a masonry layout with CSS Grid module seems quite an interesting task. I’ve covered creating masonry with CSS columns and Flexbox, but this is going to be different and better.

What’s so special?

The only problem in masonry layouts we created before was inability to order items left-to-right.

Good news is this one is going to be a horizontal one, i.e. left-to-right masonry. The most noteworthy thing about this layout is its dependence on CSS Grid layout module. Hence the making is going to be pure CSS.

In the same vein to our flexbox masonry, a small bit of JS can be added for more practical and hassle-free sizing.

Masonry with CSS Grid Layout: The Concept

CSS Grid layout is unarguably the best way today to create different grid layouts. If you know how it works, you might have already used it for equal-height columns or holy grail layouts.

Image-only Masonry with CSS Grid
Demo the image-only masonry

While experimenting with the grid, I stumbled upon grid-row-end. This property along with grid-auto-rows sets the baseline for our masonry.

I’d like to quickly share the correct usage of CSS grid layout for beginners.

  1. Set display to grid for the container you are targeting
  2. Set the template for rows and columns using the appropriate properties

That’s it. For those who are not very well-aware of CSS Grids, here are some of my favorite resources:

  1. CSS Grid Garden, fun way to learn Grid
  2. Rachel Andrew’s Grid by example is a more in-depth and elaborated source for the same
  3. CSS Grid at MDN is also a great article to begin with

CSS

Resuming the topic, the concept revolves around a grid with automatically assigned columns. This can be done using grid-template-columns property.

Let’s take a look on some code to achieve a grid with some gap between its columns.

.masonry {
  display: grid;
  grid-gap: 2em; /* [1] Add some gap between rows and columns */
  grid-template-columns: repeat( auto-fill, minmax( 200px, 1fr ) ); /* [2] Make columns adjust according to the available viewport */
  grid-auto-rows: 250px; /* [3] Set the height for implicitly-created row track */
}

Explanation

The initial phase of our masonry grid
  1. We need some gap or gutter-space between the rows and columns of our grid, which is perfectly done by grid-gap.
  2. The columns should be flexible and respond properly to the available screen-size. Therefore we harnessed the handy repeat() and minmax() CSS functions. Above template sets columns according to whatever fits best between 200 pixels and a fraction of the grid. Our grid should be filled with such columns automatically.
  3. We have not specified anything explicitly for the rows in the CSS above. In such a case, implicit grid row tracks are created automatically to hold our grid items.

These tracks are sizeable using the grid-auto-rows property. We need to set it because we will use them later to span and size the columns to form a masonry-like effect.

Implicit tracks

Implicit tracks are automatically constructed by the grid itself when no explicit template is added for rows and/or columns.

Each track is basically a sum of the specified grid-gap and the size of the implicit track which is automatic when not specified.

Adding the masonry touch

Now that we have assigned some height to the implicit row tracks, we can make our masonry items span to one, two, or any sensible number of rows we want.


.col--2x {
   grid-row-end: span 2;
}
.col--3x {
   grid-row-end: span 3;
}

When you set span 2 for grid-row-end, you are telling the grid to make the item take two spans of the implicit row-track size. And this is how it follows with span 3 and so on and so forth.

Here is what we got after implementing the grid-row-end as mentioned above:

Masonry grid after applying 'grid-row-end'

Demo the Masonry

Did you noticed the order? It is from left to right, which was missing in our earlier trials. Amazing, isn’t it?

But again, something is lacking in this even now. This masonry isn’t practical at all to do the spanning of tiles accordingly. Imagine 50 different items in your masonry grid, each with different content size. I’m sure you too wouldn’t like setting spans manually for each of those items.

CSS Grid Layout + JavaScript = Perfect Masonry

How much should an item span to create a masonry-like effect?

Horizontal Masonry using CSS Grid
Horizontal Masonry done using CSS Grid [Demo]

Normally the grid kinda fixes the height of each item, therefore we need to use grid-row-end property to size the items. And here only, we have to use some JavaScript to calculate the appropriate span-size for each item.

We have seen already that spanning is the product of a number and the size of implicit row-tracks. All we need now is that appropriate number for each item.

.masonry {
  display: grid;
  grid-gap: 1em;
  grid-template-columns: repeat(auto-fill, minmax(200px,1fr));
  grid-auto-rows: 0;
}

I gave the implicit row-track a size zero for the sake of simplicity and sizing. This makes sense, as we already have some grid-gap scaffolding the tracks for us.

Now, in order to get the appropriate span number for a given masonry item, this is my hypothesis:

With G as the row-gap for our grid, R as the size of implicit row-tracks, and H as the height of the content of any given masonry item, the net height (H1) of our masonry item can be given as:

H1 = H + G

And the net size of each track (T) can be given as:

T = G + R

Now, an appropriate span number (S) for any given masonry item can be calculated using the above equations:

S = H1 / T

Andy Barefoot experimented with some JavaScript already to achieve a left-to-right masonry. I quickly forked through his code to make it adapt my need.

Calculating appropriate spanning for any given item

Let’s take a look at the function that calculates an appropriate span number for any given cell of our masonry. I’m providing my code as is, with documentation to make it easily understandable.

/**
 * Set appropriate spanning to any masonry item
 *
 * Get different properties we already set for the masonry, calculate 
 * height or spanning for any cell of the masonry grid based on its 
 * content-wrapper's height, the (row) gap of the grid, and the size 
 * of the implicit row tracks.
 *
 * @param item Object A brick/tile/cell inside the masonry
 */
function resizeMasonryItem(item){
  /* Get the grid object, its row-gap, and the size of its implicit rows */
  var grid = document.getElementsByClassName('masonry')[0],
      rowGap = parseInt(window.getComputedStyle(grid).getPropertyValue('grid-row-gap')),
      rowHeight = parseInt(window.getComputedStyle(grid).getPropertyValue('grid-auto-rows'));

  /*
   * Spanning for any brick = S
   * Grid's row-gap = G
   * Size of grid's implicitly create row-track = R
   * Height of item content = H
   * Net height of the item = H1 = H + G
   * Net height of the implicit row-track = T = G + R
   * S = H1 / T
   */
  var rowSpan = Math.ceil((item.querySelector('.masonry-content').getBoundingClientRect().height+rowGap)/(rowHeight+rowGap));

  /* Set the spanning as calculated above (S) */
  item.style.gridRowEnd = 'span '+rowSpan;
}

Applying appropriate spanning to each item

Now we can simply loop through all the masonry items to apply appropriate spanning. That’ll be taken care of with the help resizeMasonryItem() as declared above.

/**
 * Apply spanning to all the masonry items
 *
 * Loop through all the items and apply the spanning to them using 
 * `resizeMasonryItem()` function.
 *
 * @uses resizeMasonryItem
 */
function resizeAllMasonryItems(){
  // Get all item class objects in one list
  var allItems = document.getElementsByClassName('masonry-brick');

  /*
   * Loop through the above list and execute the spanning function to
   * each list-item (i.e. each masonry item)
   */
  for(var i=0;i<allItems.length;i++){
    resizeMasonryItem(allItems[i]);
  }
}

Resizing things when images finish loading

If an item consists of images, its content’s height will be changed once the images are completed loaded. This will result in a messed-up masonry layout.

The below JS function takes care of it and resizes the items one more time after all the images finish loading. I’m using ImagesLoaded to simply things for that matter.

/**
 * Resize the items when all the images inside the masonry grid 
 * finish loading. This will ensure that all the content inside our
 * masonry items is visible.
 *
 * @uses ImagesLoaded
 * @uses resizeMasonryItem
 */
function waitForImages() {
  var allItems = document.getElementsByClassName('masonry-brick');
  for(var i=0;i<allItems.length;i++){
    imagesLoaded( allItems[i], function(instance) {
      var item = instance.elements[0];
      resizeMasonryItem(item);
    } );
  }
}

Hooking the function on appropriate events

It’s important to tell the DOM about when to execute all three JavaScript functions we wrote above. Obviously, we want our items to be properly aligned once the window loads. But wait, what about resizing? You do want the grid look good on window-resizing as well.

/* Resize all the grid items on the load and resize events */
var masonryEvents = ['load', 'resize'];
masonryEvents.forEach( function(event) {
  window.addEventListener(event, resizeAllMasonryItems);
} );

Finally, we keep our waitForImages() function open to observe the images regardless of any events to occur.

/* Do a resize once more when all the images finish loading */
waitForImages();

Demo the CSS Grid + JS Masonry Image-only Masonry Using CSS Grid

Caveats

Even though the grid looks just perfect in the demonstrations, but it does have some pitfalls.

  1. It’s expensive. Looping through each of the several grid items and setting properties would be pretty expensive on client’s resources.
  2. The masonry won’t be visible if you set the grid-gap to 0. You have to tweak the JS to make the masonry work for smaller grid-gaps.
  3. It breaks down once the browser detects it’s taking too much time to apply settings. You have to keep the grid either short or paginated.

That’s all folks. Adding a preloader to the masonry is on my to-do list, and I’ll update the article as soon as I’m done with it.

I hope this was worth your time. Share with me your demos, your views, suggestions, and problems around and about the same topic. Do share this ahead if you found the stuff helpful. Cheers!