Demo: Dynamic Cross-browser Horizontally Fluid Menu

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 prototype of the Array object (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 array called priResizedNav. 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 pixels wide and the total width of all the navigational items is 1000 pixels than I’ll get a value of 0.2, or in other words, the width of item 1 is 20% 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 is 20% of 969 pixels which is equal to 193.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.

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:

$('&gt; li', nav).each(function (i) {
nav_item_height = ($(this).height() &gt; nav_item_height) ? $(this).height() : nav_item_height;
});

$('&gt; li', nav).each(function (i) {
if($(this).height() &lt; 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-top and padding-bottom on the hyperlink within the item, using the same top and bottom values 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!

This entry was posted in JavaScript, Labs and tagged , , , , , , , . Bookmark the permalink.

3 Responses to Demo: Dynamic Cross-browser Horizontally Fluid Menu

  1. René Dijk says:

    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.

  2. René Dijk says:

    The examples are not working. The links are broken. Could you please fix this?

    • Mike says:

      My apologies – I just recently moved servers and failed to copy that folder over! It has been reestablished and should be working properly now.

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Spam Protection by WP-SpamFree