Distributing Elements Within a Container

Posted on September 6, 2006 by Garrett Murphey
Cascading Style Sheets, Javascript

One aspect of HTML and CSS I've always felt is lacking is the ability to distribute elements evenly within its container. Using the CSS rule text-align:'justify', you are able to justify lines of text within their container, but there's really no equivalent for non-text elements.

This became a problem a few weeks ago when a client requested that their site menu span the entire width of the page. Before discovering CSS, I would accomplish this using tables and images. But since then, I've learned that images are quite inaccessible and can be very difficult to maintain when the menu changes, and since this site was being built using CSS, I wanted to keep tables out of the design as much as possible. So, for the time being, I created something like the following.

HTML:
  1.     <body>
  2.         <ul id="container">
  3.             <li>Home</li>
  4.             <li>About Us</li>
  5.             <li>Services</li>
  6.             <li>Resources</li>
  7.             <li>Contact Us</li>
  8.         </ul>
  9.     </body>
  10. </html>

distribute_noscript.gif

That was all well and good, but it wasn't going to fly with the client.

After digging through CSS documentation and finding little that addresses this problem, I decided the best way to accomplish this effect was to use javascript to dynamically generate CSS. I'm hoping a feature is added to CSS soon since I'm not a big fan of using javascript for design.

While scripting this method, I made a list of characteristics I'd like to see it have.

  1. Utility
  2. Scalability
  3. Simplicity

With those goals in mind, I came up with this frame for the method.

distribute(container, elements, [direction = 'horizontal', className = null])

Container (required)
The id attribute or HTML object of the container.

Elements (required)
The tag name of the element(s) you want to distribute within container (e.g. 'li', 'img', 'div').

Direction (optional)
The direction you wish to distribute elements. This can either be 'horizontal' (default) or 'vertical'.

ClassName (optional)
If you have multiple instances of elements and only want to distribute certain elements, you may attach a class to those tags and pass that to the method. This property is null by default.

Let's take a look at the script (the following script was written with Prototype).

JAVASCRIPT:
  1. var containerDimen = Element.getDimensions(container);
  2. var element = $A($(container).getElementsByTagName(elements));
  3.  
  4. if (direction == 'vertical') {
  5.     var containerHeight = containerDimen.height;
  6.     containerHeight -=
  7.     (parseInt(Element.getStyle(container, 'padding-top'))
  8.     + parseInt(Element.getStyle(container, 'padding-bottom')));
  9. } else {
  10.     var containerWidth = containerDimen.width;
  11.     containerWidth -=
  12.     (parseInt(Element.getStyle(container, 'padding-left'))
  13.     + parseInt(Element.getStyle(container, 'padding-right')));   
  14. }

The first thing we need to do is collect all elements within the container. We're using the $A() method to make the results Enumberable so we can run them through an each() loop a little later. After we've gathered all the elements, we need to calculate the container width or height (depending whether direction is set for 'horizontal' or 'vertical'). Since the box model calculates width as the sum of width and padding, we have to subtract the padding from the width to get the real width of the container.

JAVASCRIPT:
  1. var elementWidth = 0;
  2. var elementHeight = 0;
  3. var i = 0;
  4.  
  5. element.each(function (e) {
  6.     if ((className == null) || ((className != null)
  7.         && (Element.hasClassName(e, className)))) {
  8.         i++;
  9.         var elemDimen = Element.getDimensions(e);
  10.         if (direction == 'vertical') {
  11.             elementHeight += elemDimen.height;
  12.             Element.setStyle(e, {
  13.                 marginTop: 0,
  14.                 marginBottom: 0
  15.             });
  16.         } else {
  17.             elementWidth += elemDimen.width;
  18.             Element.setStyle(e, {
  19.                 marginLeft: 0,
  20.                 marginRight: 0
  21.             });
  22.         }
  23.     }
  24. });

Now that we have the container width (or height), we need to calculate the total width (elementWidth) of all the elements. If there were any margins applied to the elements for a noscript version, we remove them. i is used to count how many elements match the search criteria since className may or may not be null.

JAVASCRIPT:
  1. if (direction == 'vertical')
  2.     var leftover = containerHeight - elementHeight;
  3. else
  4.     var leftover = containerWidth - elementWidth;
  5.  
  6. var marginNum = i - 1;
  7. var margin = Math.floor(leftover / marginNum);
  8.  
  9. element.each(function (e) {
  10.     if ((className == null) || ((className != null)
  11.         && (Element.hasClassName(e, className)))) {
  12.         if (direction == 'vertical') {
  13.             if (e != element[element.length - 1])
  14.                 Element.setStyle(e, {
  15.                     marginBottom: margin + 'px'
  16.                 });
  17.         } else {
  18.             if (e != element[element.length - 1])
  19.                 Element.setStyle(e, {
  20.                     marginRight: margin + 'px'
  21.                 });
  22.         }
  23.     }
  24. });

We're just about done! With the elements and container width calculated, we figure out how much space is leftover. The leftover space is divided by i minus one (we'll only be applying margins to the right side of elements) to figure out how much space should be between each element. All that's left to do after that is to place those margins on each of the elements.

With the javascript written, all that's left to do is make a little modification to the HTML.

HTML:
  1.     <body onload="distribute('container', 'li')">
  2.         <ul id="container">
  3.             <li>Home</li>
  4.             <li>About Us</li>
  5.             <li>Services</li>
  6.             <li>Resources</li>
  7.             <li>Contact Us</li>
  8.         </ul>
  9.     </body>
  10. </html>

distribute.gif

That's it. It's probably a good idea to add default margins to the elements for users with javascript disabled (any default margins get removed by the script). Enjoy!

Resources

The script in this post uses Sam Stephenson's Prototype library. There's some very good documentation for Prototype written by Sergio Pereira and over at script.aculo.us (another great javascript library).

Download the Source

All source code is provided under the Creative Commons Attribution-Sharealike License. If you agree to these terms, please view and download the entire source now.

4 Responses to
“Distributing Elements Within a Container”

Oh my dear Garrett,
;)
use an unordered list or two for this problem, and you have no *div* soup or anything else to code.

pure css is always better.

have a nice day and read http://www.cssplay.co.uk/index.html Stu is a one of the best in css for me.

Monika

Garrett Murphey

Hi Monika: There's no solution to this problem on the site you provided.

From what I've seen, there's no way to tell CSS to distribute elements within their containers. That's why I wrote the javascript.

If anyone has a pure CSS solution, please share it. I'd much rather not have to use javascript for these kind of things.

Thanks for posting your tutorial -- it seems like it should be such an easy thing that a lot of people are doing, but yours is the only answer that google can find for me!

I still can't get it to work, though. I've copy and pasted the html and linked to the js file, but that doesnt fix the problem. Is there some CSS component that I am missing, or another js file that it is dependent upon?

Leave a Reply

If you'd like to post a code snippet in your comments, please surround the code with code language tags (e.g. [php][/php], [html][/html], [javascript][/javascript]).