Equalise column heights with jQuery

In which we offer a new solution to the perennial problem of column height equalisation

Ah, the perennial problem of equalising floated columns. Googling for ‘jquery equal column heights’ or similar already turns up a shipload of results, so I’m on well-trodden ground here. While there’s plenty of plugins that accomplish this and most likely work well in most cases, the solution here offers a little extra.

So the usual method of doing this goes something like:

$.fn.equaliseHeights = function() {
    var max = 0;
    $(this).each(function(i, v) {
        max = Math.max($(v).height(), max);
    });
    return $(this).each(function(i, v) {
        $(v).height(max);
    });
};

Nice and easy. We loop through all the passed elements, find the largest height, then set all elements to this height. Nothing wrong with this, and possibly all you need. However, there are just a couple of…

(Potential) problems or limitations

  1. You may be aware of Mr Paul Irish’s suggestion to use * { box-sizing: border-box } to use the old IE box model on all elements – that is, you define an element’s width, and paddings/borders will be subracted from this width. The above approach breaks when the elements use this box model.
  2. Another issue arises if we’re using responsive layouts. Say we’ve got a number of boxes that show side by side on larger screen resolutions, but  on top of one another on smaller screens. In the latter case we probably don’t want to be equalising the heights as the elements are no longer showing as columns, and extra padding at the bottom will just look odd.Or what if we have a number of columns, but some of them might (or might not) drop onto a new line? Quite likely I’d only want to equalise heights of the boxes that are on the same line.

The solution (or at least a first stab at one)

The plugin here addresses both of these issues; firstly, it’ll work whichever box model the target elements use. Secondly, it will only resize elements whose vertical offsets are identical, ie are on the same row. The resizing is done row by row, so it’ll still work if boxes on the second row have different top positions due to wonkiness in the row above.

The upshot is that it’s then much easier for us to equalise columns without having to wrap them in a container and reference this element. That is, we can say goodbye to the misery of

$('.row').each(function() {
    $(this).children('.col').equaliseHeights();
});

and embrace the simple concision of

$('.col').equaliseHeights();

What’s more, if this is bound to a resize event, then all columns in each row will be sized correctly however much the layout changes.

Be careful of using this in a resize event for Internet Explorer. IE triggers this event whenever the height of the body element changes which can cause the function to be recursively triggered several times, so it would be necessary to check that the window dimensions have actually changed within the resize event. See the addendum below for a fix.

However, if you do want to bloody-mindedly force everything to be the same height regardless of position, that’s no problem – just pass a single truey argument thus:

$('.col').equaliseHeights(true);

Browser compatibility

Tested in Chrome, Firefox, Opera, and IE6 and up (with the aforementioned caveat about resize events).

Demo

Okay, so let’s make us some wonky columns:

<div class="demo-container clearfix">
    <div class="demo-box"><h3>Box 1</h3><img src="http://placekitten.com/146/180" alt="" /></div>
    <div class="demo-box"><h3>Box 2</h3>Let's put uneven amounts of shit in these boxes,</div>
    <div class="demo-box"><h3>Box 3</h3>shall we?</div>
    <div class="demo-box"><h3>Box 4</h3>How about adding a second line?</div>
    <div class="demo-box"><h3>Box 5</h3>Will that work?</div>
    <div class="demo-box"><h3>Box 6</h3><img src="http://placekitten.com/146/120" alt="" /></div>
</div>

This markup leaves the layout very open – there’s nothing here that specifies rows (if there were, it would be a table-based layout using divs). Maybe there’ll be three boxes in a row, maybe two, or it may change depending on the container width, or perhaps we’re not working with a fixed number of boxes. Whatever we’re up to, we’d like the rows to be evened out however they fall.

There’s not much to the CSS, the important thing is that the inner boxes are given equal widths and floated.

.demo-box { float: left; padding: 1.7%; margin: 3.4% 0 0 3.4%; border: 1px solid red; width: 28.8%; font-size: .85em }
.demo-box:last-child { margin-bottom: 3.4% }
.demo-box-half { width: 44.9% }

Which initially yields us this beauty:

Box 1

Box 2

Let’s put uneven amounts of shit in these boxes,

Box 3

shall we?

Box 4

How about adding a second line?

Box 5

Will that work?

Box 6

Equalise! Revert Toggle 2/3 columns

Click the buttons to see the plugin do its thing. Notice that the heights of the boxes are only equalised with other boxes on the same row. No containing divs are required to specify rows as the plugin automatically groups boxes into rows based on their starting vertical positions.

The code for the buttons is simply

// equalise heights
$('#demo-go').click(function() {
    $('.demo-box').equaliseHeights();
    $(this).addClass('disabled');
    $('#demo-revert').removeClass('disabled');
    return false;
});
// reset heights
$('#demo-revert').click(function() {
    $('.demo-box').css({height:'auto',minHeight:0});
    $(this).addClass('disabled');
    $('#demo-go').removeClass('disabled');
    return false;
});
// toggle 2-3 columns
$('#demo-col-toggle').click(function() {
    $('.demo-box').toggleClass('demo-box-half');
    // equalise again if equalise button's been clicked
    $('#demo-go').hasClass('disabled') && $('.demo-box').equaliseHeights();
    return false;
});
// equalise on window resize
$(window).resize(function() {
    $('#demo-go').hasClass('disabled') && $('.demo-box').equaliseHeights();
});

The code

By now you must surely be salivating with anticipation, so here’s the plugin code:

// equalise column heights
// groups passed elements by vertical offset,
// and equalises the heights of all those on the same line
// call with argument set to true to prevent this and force everything to be the same height
jQuery.fn.equaliseHeights = (function($) {

    var cssProp = $.browser.msie && $.browser.version < 7 ? 'height' : 'min-height';

    // go around the bleedin houses to get the element's height
    // if box-sizing is set to border-box, and a min-height is set,
    // ff and ie revert to old box model, ie height ends up as min-height + padding
    // so test by temporarily setting min-height to current height and see if element's height changes
    // returns outerHeight() if box-sizing is set to border-box and correctly implemented, otherwise height()
    // so that the element's min-height can be set without adding extra padding
    function getHeight(elm) {
        var h    = elm.outerHeight(),
            prop = elm.css(cssProp, h).outerHeight() > h ? 'height' : 'outerHeight';
        return elm.css(cssProp, 0)[prop]();
    }

    return function(dontGroup) {
        var thiz  = this,
            group = $(),
            max   = 0;

        // loop through each element and add it to the current row group
        // if the next element doesn't exist, or has a different vertical position,
        // then equalise current group and start a new one
        return thiz.each(function(i, v) {
            var elm  = $(v),
                next = thiz[i + 1];
            group = group.add(elm);
            max   = Math.max(max, getHeight(elm));
            if (!next || (!dontGroup && elm.offset().top !== $(next).offset().top)) {
                group.css(cssProp, max);
                group = $();
                max   = 0;
            }
        });
    };

}(jQuery));

Addendum: fixing resize event in IE

As mentioned earlier, IE is just a tad trigger-happy with the resize event, firing it event every time the dimensions of the body element change, and up to three times when the window actually is resized. This means that if the equalisation were bound to the resize event, it could end up triggering further “resizes”, effectively calling itself recursively. To fix this we can bind the resize event to a function which will check if the window size has in fact changed before triggering a custom event which can be called “trueresize”. Our equalisation plugin (or anything else) can then be bound to this trueresize event.

(function() {
    var win = $(window),
        w   = win.width(),
        h   = win.height();
    function hasChanged() {
        var w2      = win.width(),
            h2      = win.height(),
            changed = (w !== w2 || h !== h2);
        w = w2; h = h2;
        return changed;
    }
    $(window).resize(function(e) {
        hasChanged() && $(window).trigger('trueresize');
    });
}());

$(window).bind('trueresize', function() {
    $('.col').equaliseHeights();
});