Posts / April 3, 2019

Responsive Grid For Varying Height Elements In Bootstrap4

The quest for developers to handle varying height elements in a responsive grid is a lot like looking for the holy grail. Everyone is looking (including myself) for the perfect solution that’s also fast and lightweight.

Searching for responsive grid for varying height elements, the quest for the holy grail.

For a detailed explanation of the problem, I’d recommend reading Varying Column Heights in Bootstrap by Carol Skelly. She does a great job outlining the problem and providing some possible solutions. 👍

One of those solutions that Carol points out is Masonry, which is often people’s default solution. I’ve used it in the past although based on the search results for “masonry alternatives” it’s pretty easy to see it’s not the perfect solution. Although Masonry is incredibly robust it’s also really big including features and bloat that’s just not required in most projects.

So in the past, my goto solution to handle varying height elements has been Grid-A-Licous created by Andreas Pihlström. It’s a great library that’s been around since 2009. It’s fairly lightweight and has handled most of my needs on the different projects I’ve worked on.

In Rails, I would generally do something like:

<div id="cards" data-width="350" data-gutter="50">
  <% @cards.each do |card| %>
    <div class="card">
      <div class="card-body">
        <div class="card-title"><%= card.title %></div>
        <p class="card-text"><%= card.description %></p>
      </div>
    </div>
  <% end %>
</div>

And then in my Coffeescript I have something like:

gridCardsReady = ->
  if $('div[data-width]').length > 0
    $cards = $("div[data-width]")
    $cards.gridalicious({
      width: $cards.data('width'),
      selector: '.card',
      gutter: $cards.data('gutter'),
      animate: true,
      animationOptions: {
        speed: 80,
        duration: 100,
        queue: true
    }});
$(document).ready(gridCardsReady);
$(document).on('turbolinks:load', gridCardsReady);

And voila, Grid-A-Licous takes care of the rest! Changing the page size automatically adjusts and repositions the elements correctly into their columns. To have more or less columns, I simply change the data-widthattribute and it handles calculating the correct number of columns that fit. To add more padding between elements I simply increase the data-gutter attribute. As Pete the Cat says, “it’s all good!”

One of my minor complaints with Grid-A-Licous is with the padding, for example when you scale down to a single column, there’s still left and right padding. Ideally there would be no padding or margin so the elements in a single column are aligned with other page elements above and below. I’ve often gotten around this by not having margins or padding on that section with the cards, but it’s still a small annoyance.

Recently though I ran into another problem using Grid-A-Licous with lazysizes, a lazy loading image framework. Unfortunately the lazy loading of images combined with the repositioning of elements based on their height, caused a random ordering of elements. In the case where ordering of element wasn’t important then this wasn’t a big deal. But for the times when ordering was important, the solution kind of fell apart.

So this led me back on the holy grail quest for another solution. After some searching and digging I stumbled onto Colcade, a lightweight masonry layout. With Colcade you define your columns in HTML and CSS for various breakpoints, and it handles moving elements into the appropriate visible column. On the initial tests things worked well, cards loaded fast and responsiveness was handled as expected. 👌

In Colcade, you normally define your responsive columns in CSS:

/* Using floats */
.grid-col {
  float: left;
  width: 50%;
}

/* 2 columns by default, hide columns 2 & 3 */
.grid-col--2, .grid-col--3 { display: none }

/* 3 columns at medium size */
@media ( min-width: 768px ) {
  .grid-col { width: 33.333%; }
  .grid-col--2 { display: block; } /* show column 2 */
}

/* 4 columns at large size */
@media ( min-width: 1080px ) {
  .grid-col { width: 25%; }
  .grid-col--3 { display: block; } /* show column 3 */
}

Although since I’m using Bootstrap, I really didn’t want to define my own column widths when the rest of my site is using Bootstrap’s grid system. So this led me to come up with Bootstrap flavoured columns like:

<div id="cards" class="row">
  <div class="col col-sm-12 col-md-6 col-lg-4 d-block"></div>
  <div class="col col-md-6 col-lg-4 d-none d-sm-none d-md-block"></div>
  <div class="col col-lg-4 d-none d-sm-none d-md-none d-lg-block"></div>
  <% @cards.each do |card| %>
    <div class="card">
      <div class="card-body">
        <div class="card-title"><%= card.title %></div>
        <p class="card-text"><%= card.description %></p>
     </div>
    </div>
  <% end %>
</div>

This example would generate 3 columns on lg, 2 columns on md, and 1 column on sm. And then in my Coffeescript I now have:

gridCardsReady = ->
  if $('#cards').length > 0
    $('#cards').colcade({
      columns: '.col',
      items: '.card'
    });
$(document).ready(gridCardsReady);
$(document).on('turbolinks:load', gridCardsReady);

That’s it! I liked this approach a lot better than defining a bunch of my own floats and widths in CSS, instead just use Bootstrap’s col class. This makes it easy to your columns, for example you might want two columns on small width. And the good news is it also works with lazysizes, so elements with lazy loaded images still maintain their order!