The following is a solution that I came up with create a fluid horizontal navigation structure that met the following requirements:
- The navigation needed to be completely dynamic and not dependent on an update to a CSS file if items were added or removed from it.
- Must horizontally span the entire width defined by the page layout.
- Each navigational item has to be horizontally and vertically centered within it’s ‘cell’.
- No ‘cheating’ by using a
<table>for a layout construct. - Support as many browsers as possible – including IE6.
I spent some time trying to find a workable solution and I was surprised that I couldn’t find any that met most of what I needed. Perhaps thats is more of a testament to my searching “skills”? Anyhow, creating my own solution and re-inventing the proverbial wheel seemed more interesting to me anyways!
If browser support wasn’t an issue, this would have been a fairly easy task to complete – after all, you can simulate a table layout by using the display: table operation along with table-row and table-cell. However, neither IE6 or IE7 support these layout types.
Based on the requirements that this navigation had, doing a straight-up HTML and CSS solution was not going to work. It was pretty clear that there would need to be some DOM manipulation required via JavaScript.
The HTML and CSS for the navigation
For the site that I implemented this solution on, there was dropdown navigation for each of the ‘primary’ items. However, in my implementation for this example I’ll be assuming there is no dropdown navigation involved. However, this doesn’t really affect or change the way the elements will be styled – but there is one important thing to keep in mind which I’ll get to later.
<div id="wrapper"> <ul class="navigation"> <li><a href="#">Home</a></li> <li><a href="#">About the Company</a></li> <li><a href="#">News Releases</a></li> <li><a href="#">Investors Information</a></li> <li><a href="#">Policies and Producedures</a></li> <li><a href="#">Contact Us</a></li> <li><a href="#">Site Map</a></li> </ul> </div>
Nothing to out of the ordinary – just a simple unordered list within a wrapper which will be used to define the width of the layout.
#wrapper {
margin: 0 auto;
width: 969px;
}
ul.navigation {
background-color: #e1e1e1;
list-style-type: none;
margin: 0;
padding: 0;
overflow: hidden;
position: relative;
width: 100%;
}
ul.navigation li { float: left; text-align: center; }
ul.navigation a {
display: block;
color: #666;
padding: 6px 10px;
text-decoration: none;
}
ul.navigation a:hover { background-color: #f1f1f1; }
The styling of the navigation is kept very simple – I’m basically just floating each item in the navigation and then applying the necessary padding to the item’s hyperlink. To keep things simple and clean, it’s best to not apply any borders, padding or margins to the list items. Otherwise, the script which dynamically resizes them will need to account for this (box model), and currently it is not setup for that. I recommend that if you require extra elements with which to style your navigation with that you just use <span> tags within the <li> tag itself.
Demo 1: Simple horizontal navigation
A working example of the navigation created thus far can be viewed here.
This demo works well across all browsers and all the items in the navigation (depending on your default font size) fit on the same horizontal plane. If we disregard the requirement of the navigation spanning the entire width of the wrapper, this solution works fine.
Demo 2: Breaking the layout
Things change, however, when extra items are added and we can see rather quickly that a need for some DOM manipulation will be necessary, as shown in the example here.
True, this is a rather excessive amount of navigational items, but if you discount that fact and consider that the user could have a larger-than-defined-font-size, or, if the site is in multiple languages the labels for each item could be considerably longer, causing it to break the layout. Either way you look at it, this layout does not allow the navigation to be ‘dynamic’ – you would have to manually adjust the width of each of the <li> elements within the CSS.
Using a JavaScript-based solution
Using JavaScript to fix the layout problems is not a preferred way of doing things, but if this layout needs to work across a wide range of browsers that include IE6 and IE7 then it is the only way that it can be accomplished. Besides, this is a purely visual solution and will not impose any accessibility issues with users who do not have JavaScript enabled on their browser or handheld device (they’ll just see the navigation as was shown in the second demo).
The first thing to do is to include a reference to the jQuery library:
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
Next, the actual JavaScript that formats the navigation to meet the requirements:
Array.prototype.sum = function() {
return (! this.length) ? 0 : this.slice(1).sum() +
((typeof this[0] == 'number') ? this[0] : 0);
};
function menu() {
var nav = $('ul.navigation'),
nav_item_height = 0,
numMenuItems = $('> li', nav).size(),
totalMenuItemWidth = 0,
menuWidthRemainder = 0,
wrapperWidth = 969,
maxNavItemWidth = 200,
priNavItems = [];
/* First, determine the total width of each item in the navigation. */
$('> li', nav).each(function (i) {
totalMenuItemWidth += $(this).width();
priNavItems.push(($(this).width() > maxNavItemWidth) ? maxNavItemWidth : $(this).width());
});
/* Primary navigation items (combined) are less width than the
wrapper. */
if(totalMenuItemWidth < wrapperWidth) {
menuWidthRemainder = wrapperWidth - totalMenuItemWidth;
$('> li', nav).each(function() {
var tmp_width = $(this).width();
$(this).width(tmp_width + parseInt(menuWidthRemainder / numMenuItems));
});
tmp_width = $('> li:first', nav).width();
$('> li:first', nav).width(tmp_width + (menuWidthRemainder % numMenuItems));
}
/* Primary navigation items (combined) are greater than the width of
the wrapper. */
else if(totalMenuItemWidth > wrapperWidth) {
var priResizedNav = [];
for(var x = 0; x < numMenuItems; x++) {
/* Always take the lowest value as the sum total of the resized
navigation items must never exceed the wrapper width. */
priResizedNav.push(Math.floor((priNavItems[x] / priNavItems.sum()) * wrapperWidth));
}
$('> li', nav).each(function(i) {
$(this).width(priResizedNav[i]).css('wordWrap', 'break-word');
});
$('> li:first', nav).width(priResizedNav[0] + (wrapperWidth - priResizedNav.sum()));
}
// Ensure top-level navigation spans the available width.
$('> li', nav).each(function (i) {
nav_item_height = ($(this).height() > nav_item_height) ? $(this).height() : nav_item_height;
});
$('> li', nav).each(function (i) {
if($(this).height() < nav_item_height) {
// Default padding: 6/6 (top/bottom).
var paddingTop = parseInt((nav_item_height - $(this).height()) / 2);
var paddingBottom = parseInt((nav_item_height - $(this).height()) - paddingTop);
$('a', this).css({
paddingTop: paddingTop + 6 + 'px',
paddingBottom: paddingBottom + 6 + 'px'
});
}
});
}
The following sections (and demos) will take you through each part of the script and breakdown what I was trying to accomplish.
The
sum prototypeof theArrayobject (at the top of the source code) is extraneous to this solution and was picked up from a third-party source. I use this as a ‘helper’ throughout my solution.
Determining the width of the navigation
$('> li', nav).each(function (i) {
totalMenuItemWidth += $(this).width();
priNavItems.push(($(this).width() > maxNavItemWidth) ? maxNavItemWidth : $(this).width());
});
The first block of the script is used to determine what the actual width of each item in the navigation is at run-time. Each item’s width (pixels) is stored within an array as well as tracking a running tally of the overall width that is defined in a separate variable.
When tracking the width of each item, the value of the maxNavItemWidth variable is used to determine if the current item’s width is greater than the variable’s value (currently defined as 200 pixels). This ensures that no single item in the navigation will be larger than that pre-defined value (200 pixels). The reason for this is if you have an item that has a very long string/label. Instead of taking up all the available real-estate (and causing the navigation to wrap onto multiple lines or create awkward spacing issues) it instead can span no wider than the value that has been defined with the variable.
If the determined width is less…
There is one of two paths the script will take once the width of the navigation has been calculated. In the case where the navigation has been found to be less than the wrapper’s width (which we defined in this example as being 969 pixels), the following occurs:
if(totalMenuItemWidth < wrapperWidth) {
menuWidthRemainder = wrapperWidth - totalMenuItemWidth;
$('> li', nav).each(function() {
var tmp_width = $(this).width();
$(this).width(tmp_width + parseInt(menuWidthRemainder / numMenuItems));
});
tmp_width = $('> li:first', nav).width();
$('> li:first', nav).width(tmp_width + (menuWidthRemainder % numMenuItems));
}
- First, calculate the amount of pixel space that is remaining in the navigation and assign this value to a variable (
menuWidthRemainder). - Enumerate through each item in the navigation and assign it’s current width plus the extra width (the amount calculated by dividing it by the number of items in the navigation).
- Note: The first item in the navigation also receives the remainder from the calculation preformed in the step above.
A working example of the navigation created thus far can be viewed here. You will see how the items are all ‘stretched’ so that they fill the entire width of the navigational area.
…if the determined width is greater.
For navigation that is greater in width than what is allowed by the wrapper, a slightly more complicated solution is needed.
else if(totalMenuItemWidth > wrapperWidth) {
var priResizedNav = [];
for(var x = 0; x < numMenuItems; x++) {
priResizedNav.push(Math.floor((priNavItems[x] / priNavItems.sum()) * wrapperWidth));
}
$('> li', nav).each(function(i) {
$(this).width(priResizedNav[i]).css('wordWrap', 'break-word');
});
$('> li:first', nav).width(priResizedNav[0] + (wrapperWidth - priResizedNav.sum()));
}
- The script enumerates through the list of items in the navigation and generates a new width value that is stored within an
arraycalledpriResizedNav. The value is determined like so:- Take the initial width and divide this by the total width (priNavItems.sum) of the navigation, giving us a percentage value. For example, if item 1 is
200 pixelswide and the total width of all the navigational items is1000 pixelsthan I’ll get a value of0.2, or in other words, the width of item 1 is20%the width of the entire navigation. - With this calculated value (i.e.
0.2), I then apply it to the maximum width of the navigation as a whole, which in our demo is 969 pixels. In other words, the width of item 1 is20%of969 pixelswhich is equal to193.8 pixels. - Lastly, since this value in most cases, as in our example, is a decimal, I just take the integer value and round down (
Math.floor) to ensure the resized values of the navigation don’t exceed the allowed total width.
- Take the initial width and divide this by the total width (priNavItems.sum) of the navigation, giving us a percentage value. For example, if item 1 is
A working example of the navigation created thus far can be viewed here.
You’ll recall earlier in this post that I mentioned that I had created this solution for a menu that had dropdown navigation. You may have wondered why I simply didn’t just apply a style of overflow: hidden to the <li> element in order to get around the expanding box-model problem that IE6 has. The reason I don’t is if I do that, the dropdown navigation gets ‘chopped off’ and is no longer visible since the overflow has been set to be hidden.
The last thing that this script needs to accomplish is to resize the height of each item in the navigation so that they are all vertically centered.
Adjusting the vertical height of the navigation
When the navigation spills outside the defined width of the wrapper, there are going to be item(s) that are on multiple lines and item(s) that fit on a single line. To have these items align vertically, the script below does the following:
$('> li', nav).each(function (i) {
nav_item_height = ($(this).height() > nav_item_height) ? $(this).height() : nav_item_height;
});
$('> li', nav).each(function (i) {
if($(this).height() < nav_item_height) {
// Default padding: 6/6 (top/bottom).
var paddingTop = parseInt((nav_item_height - $(this).height()) / 2);
var paddingBottom = parseInt((nav_item_height - $(this).height()) - paddingTop);
$('a', this).css({
paddingTop: paddingTop + 6 + 'px',
paddingBottom: paddingBottom + 6 + 'px'
});
}
});
- Enumerate through each item in the navigation and determine which one has the greatest height value (in pixels).
- Re-enumerate through the navigation and apply this height value (obtained from the step above) to each item if it is less than the calculated height.
- Finally, apply
padding-topandpadding-bottomon the hyperlink within the item, using the sametopandbottomvalues that are defined by the CSS.
View the final working example of the navigation, based on all the steps we’ve covered in this post.
Final notes and thoughts
So there you have it – a horizontally-fluid navigation that will dynamically resize to fit the items container therein. I look forward to your comments and suggestions on how I can improve this script, because I know there is a lot of room for improvement!
Thank you for your quick reply and action. You’ve written a useful and clear article! In fact one of the very, very few I could find on this subject. The extensive examples and cross-browser thinking (and compatibility) make this a real treat.
The examples are not working. The links are broken. Could you please fix this?
My apologies – I just recently moved servers and failed to copy that folder over! It has been reestablished and should be working properly now.