Wednesday, January 26, 2011

Making DIVs, using the CSS "Float Left" property, all have uniform heights; automatically adjusting to match the tallest DIV in the row

Update: I modified the code so it would work with the onresize() event


Would you believe that was the best title I could come up with? Honestly, if you can think of a better one shoot me a line - I want to hear from you.

Okay, I'm a big fan of Fluid Design. I love to design websites/webapps that flow into the space available, from giant screened desktops to mobile phones, and always look like they were designed with exactly that screen size in mind.

One thing that CSS-P brought to web design that didn't exist back when we were using invisible nested tables to do webpage layout is the ability to have rows with a dynamic number of "columns". So, for instance, if you are showing rows of thumbnails, you can have more thumbnails on a row if the browser window is wide, and fewer if the window is narrower.

The method for doing this is to cause items to "Float Left". Usually this means putting your content (say a thumbnail and label) into a DIV; and giving the DIV the CSS property of:

div.column
{
   float:left;
}

If the above were added to a page's stylesheet, the DIVs with the class "column" assigned to them would stack to the right of the item before it on the page, and if there are more than one item, each will stack to the right of the previous DIV until there is no more room on that line, and then the next will appear below that row, starting a new one.

If the DIVs do not share the same height, each row will have their tops all lined up, but will have bottoms extend downwards as much as they need to to accommodate the content they contain. And the next row, which also has all the tops lined up, will appear just below the bottom of the DIV, from the previous row, that was "tallest". Like this:

(So above the second row begins below the "tallest" item in the first row, which is c)


This isn't bad, but many designers would rather have some uniformity to the DIV sizes and have, say, the container backgrounds and borders fill in the spaces below a, b, and d.  We can do that, of course, by giving each container a specified height, instead of letting the content drive the height, but then we would have to make all the containers as tall as they are likely ever to get which isn't prefect either. 

Ideally, I think, it would be best to have all of the DIVs in a row be all be the same height, but only be as tall as is actually needed to accommodate the tallest DIV in the row.  Like this:

(in the above example, the DIVs in the first row are as tall as the tallest DIV in the row, which is c, and all the DIVs in the second row are as tall as f, which is the tallest DIV in that row)

Well, with a bit of jQuery you can have your DIVs all adjust their heights so they are uniform across the entire row. You will have to add jQuery to your page (see How jQuery Works) and add the following Javascript to your page:
var currentTallest = 0;
var currentRowStart = 0;
var rowDivs = new Array();

function setConformingHeight(el, newHeight) {
 // set the height to something new, but remember the original height in case things change
 el.data("originalHeight", (el.data("originalHeight") == undefined) ? (el.height()) : (el.data("originalHeight")));
 el.height(newHeight);
}

function getOriginalHeight(el) {
 // if the height has changed, send the originalHeight
 return (el.data("originalHeight") == undefined) ? (el.height()) : (el.data("originalHeight"));
}

function columnConform() {

 // find the tallest DIV in the row, and set the heights of all of the DIVs to match it.
 $('div.column').each(function(index) {

  if(currentRowStart != $(this).position().top) {

   // we just came to a new row.  Set all the heights on the completed row
   for(currentDiv = 0 ; currentDiv < rowDivs.length ; currentDiv++) setConformingHeight(rowDivs[currentDiv], currentTallest);

   // set the variables for the new row
   rowDivs.length = 0; // empty the array
   currentRowStart = $(this).position().top;
   currentTallest = getOriginalHeight($(this));
   rowDivs.push($(this));

  } else {

   // another div on the current row.  Add it to the list and check if it's taller
   rowDivs.push($(this));
   currentTallest = (currentTallest < getOriginalHeight($(this))) ? (getOriginalHeight($(this))) : (currentTallest);

  }
  // do the last row
  for(currentDiv = 0 ; currentDiv < rowDivs.length ; currentDiv++) setConformingHeight(rowDivs[currentDiv], currentTallest);

 });

}


$(window).resize(function() {
 columnConform();
});

$(document).ready(function() {
 columnConform();
});
(the above code assumes you have assigned the CSS class called "column" to each of the DIVs that are "Floating Left")

If you can read Javascript/jQuery, you will find the code comments will sufficiently explain how it works; but in a nutshell: The script goes through each DIV, determining which are on the same row by comparing the X value of the top of each container.  It keeps track of which is the tallest, and then sets the height for each DIV in the row based on that value.

20 comments :

  1. Problem: http://jsbin.com/ijoru4

    Solution: http://jsbin.com/ijoru4/2

    I cleaned it up just a bit.

    ReplyDelete
  2. Suggested Title: Div Rows from Heaven - Equal Heights Give CSS Layouts Equal Rights!

    ReplyDelete
  3. That's a good one Dylan. I'm not sure about the "Equal Rights" part though. Maybe the title should mention Fluid Design

    ReplyDelete
  4. Absolutely the best solution for equal heights! Respect!

    ReplyDelete
  5. Guys, thanks for the effort but this solution is useless if you're trying to implement it in a responsive design using percentages for the width of the container being floated and having absolutely items inside that container.

    ReplyDelete
  6. lol, why's that?

    and what do you mean by responsive? You mean if you are resizing the windows? or if you have content that is dynamically changing in size? or just the original render?

    ReplyDelete
  7. Correct. I'm working on a responsive design that resizes according to window size. I have a ribbon done in css3 positioned absolutey so you floated container overlaps that other ones. I can show a screenshot if you like.

    ReplyDelete
  8. Did you test this option and find it didn't perform? I'm interested in your issue.

    I can say that I've tried several tests using the above method, resizing the browser window, on pages with several floating divs (with variable numbers of divs on each row depending on the space available). I'm very pleased with the response time; which seems instant to me. The Javascript in modern browsers seems stellar now-a-days (thank-you Chrome!).

    ReplyDelete
  9. If you want to send me a screenshot/link but you want to leave me a personal message, you can contact me using my Google Profile page:

    http://www.google.com/profiles/StephenAkins#contact

    ReplyDelete
  10. This comment has been removed by the author.

    ReplyDelete
  11. It seems that dealing with the DIV's using CSS is much much simpler and interesting after reading ur blog.Thanks for sharing them worthful.

    ReplyDelete
  12. Seems that this script fails when using it within an accordion from jquery.ui. All closed elements of the accordion set the heights to 0.

    ReplyDelete
  13. why it doesn't work on IE6?

    ReplyDelete
  14. This seems to be breaking in Chrome. All the divs are getting set to a height of 0px. Any Ideas?

    ReplyDelete
    Replies
    1. Very odd... just Chrome? hmmm.. possibly it has something to do with your style sheet. The following page uses the same code and it works in my Chrome. http://accessvote.com/check_test.php

      Delete
  15. Hi Stephen.

    This script is great. I particularly like how you've sorted by notional 'row' rather than equalised all divs.

    But I've had trouble using it in a slightly different context. I have 6 left-floating s within a container, and this translates to 2 rows by 3 columns. The page is 'responsive' - ie all widths are given % values, whereas I believe yours are fixed, within a flexible container.

    The first page view is great - the script does exactly what I was hoping for. But on screen resize, the heights are 'stuck' at their previous settings. So... if I narrow the window, the contents break out of their old heights, and if I widen the window, the content is way short of the container. If I refresh the window, however, everything snaps into place.

    It seems to me that on resize, we're looking to flush out some stored values, or somehow revert to original computed height settings, but this isn't happening (maybe because we still have the same elements in each row?). Like I say, a screen refresh works, but users won't realise this. So, I'm thinking a 'reset' of values would help with the window resize function.

    I'm thinking something along these lines:

    $(window).resize(function() {
    $('div.column').each(function() {
    css(“height”,”auto”); 
    });
    columnConform();
    });

    ... but this isn't working

    It'd be great to know whether you have a solution for this - I've searched elsewhere; the same problem is brought up here, but not resolved.

    http://stackoverflow.com/questions/6041654/achieving-equal-height-columns-in-a-responsive-flexible-layout

    What do you think?

    Thanks in advance.

    Max

    ReplyDelete
  16. Guys, you're right; it doesn't work correctly if the content changes the height after the page loads. So the jQuery accordion and dynamically resizing content makes it fail.

    I'm going to think about it and maybe write a new solution. I'll probably use .change() on the containing DIV to detect the contents change.

    ReplyDelete
    Replies
    1. Hi,

      Seems to me that appending the css code in the html isn't allowing it to be refreshed, so it'll retain the values of the first load of the page- at least that's my experience.

      Does this approach help?

      http://stackoverflow.com/questions/9847677/jquery-how-do-i-set-a-variable-on-page-load-on-page-resize-destroy-the-variabl

      Delete
  17. I think I have a solution for responsive designs and onresize. I changed the columnConform function a bit so it will first reset its elements originalHeight data and set height to auto.

    function columnConform() {
    var currentTallest = 0,
    currentRowStart = 0,
    rowDivs = [],
    elements = $('div.column');

    // first remove originalHeight data and reset height
    elements.removeData('originalHeight').height('auto');

    // find the tallest DIV in the row, and set the heights of all of the DIVs to match it.
    elements.each(function() {
    var $this = $(this);
    if(currentRowStart != $this.position().top) {
    // we just came to a new row. Set all the heights on the completed row
    for(currentDiv = 0 ; currentDiv < rowDivs.length ; currentDiv++)
    setConformingHeight(rowDivs[currentDiv], currentTallest);

    // set the variables for the new row
    rowDivs = []; // empty the array
    currentRowStart = $this.position().top;
    currentTallest = getOriginalHeight($this);
    rowDivs.push($this);
    } else {
    // another div on the current row. Add it to the list and check if it's taller
    rowDivs.push($this);
    currentTallest = (currentTallest < getOriginalHeight($this)) ? (getOriginalHeight($this)) : (currentTallest);
    }
    // do the last row
    for(currentDiv = 0 ; currentDiv < rowDivs.length ; currentDiv++)
    setConformingHeight(rowDivs[currentDiv], currentTallest);
    });
    }

    ReplyDelete
  18. Weirdly this all works great at home on my mamp server on the net I'm using the google pagespeed service and it manages to get the first row right but never the second. I've been scratching my head for days...

    ReplyDelete