Logo Advies en zo, Meedenken en -doen

Accessible fold-out menu using CSS and Javascript

What I wanted

I wanted a properly accessible menu using standards compliant code with a proper division between content, layout and interaction.
I have defined this to mean the following:

The methods used have been largely inspired by code and tutorials written by Peter-Paul Koch (PPK): Quirksmode.org.

See it in action

A sample of the result of these efforts is the (fake) menu on this page.

Download working sample file

Download sample (zip 10kb)

The (x-)html code

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
	<meta http-equiv="Content-Style-Type" content="text/css" />
	<meta http-equiv="Content-Script-type" content="text/javascript" />
	<link type="text/css" rel="stylesheet" href="style.css" />
	<script type="text/javascript" language="JavaScript" src="functions.js"></script>
</head>
<body>

<div id="navigation">
	<ul id="menu">
		<li><a href="index.html">Item</a></li>
		<li>
			<a href="#">Item with 1-layer submenu</a>
			<ul>
				<li><a href="index.html">Submenu item</a></li>
				<li><a href="index.html">Submenu item</a></li>
			</ul>
		</li>
		<li>
			<a href="#">Item with 3-layer submenu</a>
			<ul>
				<li><a href="index.html">Submenu item</a></li>
				<li>
					<a href="#">2-layer submenu</a>
					<ul>
						<li><a href="index.html">Submenu item</a></li>
						<li>
							<a href="#">1-layer submenu</a>
							<ul>
								<li><a href="index.html">Submenu item</a></li>
								<li><a href="index.html">Submenu item</a></li>
								<li><a href="index.html">Submenu item</a></li>
							</ul>
						</li>
					</ul>
				</li>
			</ul>
		</li>
		[etc]
	</ul>
</div>

</body>
</html>

Explanation

Indicate that the page uses proper xhtml:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

Tell the page that css and javascript is used and link in the relevant external files.
It is important to first include the main css file and only after that to include the javascript file.

	<meta http-equiv="Content-Style-Type" content="text/css" />
	<meta http-equiv="Content-Script-type" content="text/javascript" />
	<link type="text/css" rel="stylesheet" href="style.css" />
	<script type="text/javascript" language="JavaScript" src="functions.js"></script>

Set up the navigation division. This division will be positioned absolutely using css.
Yeah, I know I could have left out this extra div and put the positioning on the menu ul, however as we need margin and padding, that would make the css overly complicated to compensate for the different interpretation of the box-model by different browsers. This navigation container div saves us that headache.

<div id="navigation">
	....
</div>

The actual menu in list form.
Please note: the only conditions you need to comply with to make this menu work are:

You can use any tag-attributes you like within the menu, such as target, title etc. This won't interfere with the functioning of the menu.

If you want, you can also replace href="#" with a page address. Mind: clicking on the link will take the user to the page address.

	<ul id="menu">
		<li><a href="index.html">Item</a></li>
		<li>
			<a href="#">Item with 3-layer submenu</a>
			<ul>
				<li><a href="index.html">Submenu item</a></li>
				<li>
					<a href="#">2-layer submenu</a>
					<ul>
						<li><a href="index.html">Submenu item</a></li>
						<li>
							<a href="#">1-layer submenu</a>
							<ul>
								<li><a href="index.html">Submenu item</a></li>
								<li><a href="index.html">Submenu item</a></li>
								<li><a href="index.html">Submenu item</a></li>
							</ul>
						</li>
					</ul>
				</li>
			</ul>
		</li>
		[etc]
	</ul>

The CSS for the degraded (non fold-out) version of the menu

The CSS file you include from your xhtml file should contain the menu styling for the non-javascript, i.e. non fold-out version of the menu. This can be anything you like, as long as you remember to override the relevant styling in your additional css file.

Some styles you may want to persist in both the non fold-out as well as the fold-out version of the menu. Those styles should be put in this CSS file.

Persistent styles used in this example:

Cancel out all browser-dependant margins and padding. This is especially important for the layout of the menu-list as various browsers have a variety of standards for the margins and padding of UL and LI.

* { margin: 0px; padding: 0px; }

Give the navigation division an absolute position and a width.

#navigation {
	position: absolute;
	top: 150px;
	left: 15px;
	width: 165px;
}

Give the content division (i.e. the division which contains this text) some margin: especially a left margin which allows enough space for the menu.

#content { margin: 40px 40px 0px 220px; }

Give the menu list a relative position in relation to the parent layer, i.e. the navigation division, and give it some margin, padding and other styling.

#menu {
	position: relative;
	top: 0px;
	left: 0px;
	margin: 6px 0px 2px 0px;
	padding: 3px 4px 3px 22px;
	border: 1px solid #000000;
	color: #FFFFFF;
	background-color: #CC0000;
	list-style-type: square;
}

Give the anchors in the menu a significantly different style from the anchors in the main text.

#menu A {
	font-weight: bold;
	text-decoration: none;
	color: #FFFFFF;
	background-color: transparent;
}
#menu A:visited { color: #FFFFFF; background-color: transparent; }
#menu A:active { color: #FFFFFF; background-color: transparent; }
#menu A:hover { color: #FFFF99; background-color: transparent; }

The alternative CSS for the fold-out version of the menu

Again, you can add as much styling to the fold-out menu as you like, there are only a few styles which are really needed for the menu to work. What is needed is explained below.

UL#menu { visibility: hidden; 	list-style-type: square; }

#menu A { display: block; line-height: 10pt; }
#menu A:hover, #menu A.unfolded, #menu A.unfolded:hover { color: #FFFF99; background-color: transparent; }

#menu LI { position: relative; margin: 0px; padding: 3px 0px 3px 0px; }

#menu LI UL {
	display: none;
	position: absolute;
	top: 0px;
	border: 1px solid #000000;
	padding: 1px;
	list-style-type: none;
	color: #FFFFFF;
	background-color: #CC0000 !important;
}

#menu LI UL { left: 120px; width: 110px; z-index: 100; }
html>body #menu LI UL { left: 90px; }

#menu LI UL LI UL { left: 90px; z-index: 200; }
html>body #menu LI UL LI UL { left: 75px; }

Explanation

Set the visibility of the complete menu to hidden. This way the menu will not be visible until the javascript functions have been assigned to it.

UL#menu { visibility: hidden; }

Change the display of the anchors to block to make the mouse-over area larger. (Buggy in IE)

#menu A { display: block; }

Create an unfolded class style which is the same as the :hover style which is used when a submenu is folded out. (Buggy in IE)

#menu A:hover, #menu A.unfolded, #menu A.unfolded:hover { color: #FFFF99; background-color: transparent; }

Give the list-items in the menu position relative. This is necessary for the absolute positioning of the submenu's to work.

#menu LI { position: relative; }

Hide the submenu's by setting their initial display style to none.
Make their position absolute in relation to their positioned LI container.
Set a color and background-color so that any underlying text in the main content will be covered up.

#menu LI UL {
	display: none;
	position: absolute;
	top: 0px;
	color: #FFFFFF;
	background-color: #CC0000 !important;
}

Set the width of all submenu's.
Set the left position of the first level submenu's which is set in relation to the main menu width (which was set in the first css file to 165px). As IE does not take persist the background-color over the text of other menu-item properly, we need to make the IE left styling bigger than that which is needed for Mozilla based browsers or Opera, where we can allow for more overlap of the menu's. The extra overlap for these browser is gained by changing the left position through the html>body selectors which are ignored by IE.
Set the z-index to 100 to make sure it position itself on top of other items.

#menu LI UL { left: 120px; width: 110px; z-index: 100; }
html>body #menu LI UL { left: 90px; }

Set the left position of the lower level submenu's which is set in relation to the submenu width which we just set to 110px. Again we make a correction for IE's faulty background-color persistance.
Set the z-index to 200 to make sure it position itself on top of other items.

#menu LI UL LI UL { left: 90px; z-index: 200; }
html>body #menu LI UL LI UL { left: 75px; }

The javascript

if (document.getElementById && document.getElementsByTagName) {
	document.write("<link type=\"text/css\" rel=\"stylesheet\" href=\"jsmenustyle.css\" />");
}

window.onload = init;

function init() {
	if (document.getElementById && document.getElementsByTagName) {
		var myMenu = document.getElementById("menu").getElementsByTagName("A");
		if (!myMenu) { return; }
		else {
			for (var i=0;i<myMenu.length;i++) {
				myMenu[i].onmouseover = navHoverStyle;
				myMenu[i].onfocus = navHoverStyle;
			}
			document.getElementById("menu").style.visibility = "visible";
		}
	}
}

// Stores the currently open UL objects
var openMenus = new Array();

// Stores the timer for closing the menu
var navTimer;

function navHoverStyle(e) {
	if (!e) var e = window.event;
	if (e.target) var tg = e.target;
	else if (e.srcElement) var tg = e.srcElement;

	var linkElm = tg;
	while (linkElm.nodeName != 'A')
		linkElm.parentNode;

	while (tg.nodeName != 'LI')
		tg = tg.parentNode;

	// Determine if and if so, which submenu items to close
	var tgParent = tg.parentNode;
	while (tgParent.nodeName != 'UL')
		tgParent = tgParent.parentNode;

	if (tgParent.id == 'menu') {
		closeAll(0);
	}
	else {
		var j=0;
		while (openMenus[j] != tgParent) {
			j++;
		}
		closeAll(j+1);
	}

	// Determine if the current item has a submenu and if so, open it
	for ( var i=0;i<tg.childNodes.length;i++) {
		if ( tg.childNodes[i].nodeName == 'UL') {
			var subMenuElm = tg.childNodes[i];
		}
	}

	if (subMenuElm) {
		linkElm.classname = 'unfolded';
		subMenuElm.style.display = 'block';
		openMenus.push(subMenuElm);
	}

	// Set the timer
	checkNavTimer();

	return false;
}

function checkNavTimer() {
	if (navTimer) clearTimeout(navTimer);
	navTimer = setTimeout('closeAll(0)',5000);
}

function closeAll(lvl) {
	var oMl = openMenus.length-1;
	for ( var i=oMl;i>=lvl;i--) {
		var linkElm = openMenus[i].previousSibling;
		while (linkElm.nodeName != 'A')
			linkElm = linkElm.previousSibling;

		linkElm.className = '';
		openMenus[i].style.display = 'none';
		openMenus.pop();
	}
}

Explanation of the initiation of the menu functions and variables

If the required DOM methods are available, load the alternative stylesheet which contains the css for the fold-out version of the menu.

if (document.getElementById && document.getElementsByTagName) {
	document.write("<link type=\"text/css\" rel=\"stylesheet\" href=\"jsmenustyle.css\" />");
}

Once the page has loaded we run the init function.

window.onload = init;

We test (again) for the required DOM methods and if they are available we gather all anchor elements within the menu list into an array called myMenu.

function init() {
	if (document.getElementById && document.getElementsByTagName) {
		var myMenu = document.getElementById("menu").getElementsByTagName("A");

If no anchor elements where found, we end the function.

		if (!myMenu) { return; }

When anchor elements have been found, we go through them and assign the navHoverStyle function to each and every one of them for the onmouseover and onfocus events.
Assigning the function to the onfocus event will ensure that the function will also work if the user tabs through the menu links.

		else {
			for (var i=0;i<myMenu.length;i++) {
				myMenu[i].onmouseover = navHoverStyle;
				myMenu[i].onfocus = navHoverStyle;
			}

In the alternative stylesheet we made the menu invisible. Now the functions have been assigned to the menu, make it visible again.

			document.getElementById("menu").style.visibility = "visible";
		}
	}
}

Set up an array which will remember which submenu's have been folded out (opened).

// Stores the currently open UL objects
var openMenus = new Array();

Set up a variable which will remember when the last mouseover/onfocus action occurred.

// Stores the timer for closing the menu
var navTimer;

Explanation of the navHoverStyle function

Each time a user mouses over or focuses on a link in the menu, the main processing function navHoverStyle() is called.

function navHoverStyle(e) {

We start by determining which event fired the function and walk the DOM tree to get the associated anchor element and the surrounding list-item element.

	if (!e) var e = window.event;
	if (e.target) var tg = e.target;
	else if (e.srcElement) var tg = e.srcElement;

	var linkElm = tg;
	while (linkElm.nodeName != 'A')
		linkElm.parentNode;

	while (tg.nodeName != 'LI')
		tg = tg.parentNode;

Next we get the parent UL element of the current list-item element as that is the actual submenu layer we are currently on.

	var tgParent = tg.parentNode;
	while (tgParent.nodeName != 'UL')
		tgParent = tgParent.parentNode;

If the current 'submenu' layer is the menu container (the UL with id="menu"), we are not on a submenu layer, so we close all open submenu's.

	if (tgParent.id == 'menu') {
		closeAll(0);
	}

Otherwise, the current submenu layer is one of the underlying submenu UL's.
Go through the openMenus array to find this element and close all submenu's below this one.

	else {
		var j=0;
		while (openMenus[j] != tgParent) {
			j++;
		}
		closeAll(j+1);
	}

Now we go through the childNodes of the moused-over / focused list-item to determine if the current list-item has a submenu.

	for ( var i=0;i<tg.childNodes.length;i++) {
		if ( tg.childNodes[i].nodeName == 'UL') {
			var subMenuElm = tg.childNodes[i];
		}
	}

If a submenu element was found, we want a couple of things to happen:
We want the mouse-over style to persist for the link associated with the submenu to be opened, so that when the user goes to the newly opened menu, the user will always knows where he is. We therefore set the class attribute of the moused-over anchor element to 'unfolded' which has the same style as a:hover.

	if (subMenuElm) {
		linkElm.className = 'unfolded';

We change the display style of this submenu we found to block to make it visible to the user.

		subMenuElm.style.display = 'block';

And add the submenu element to the openMenus array, so we know what we've opened.

		openMenus.push(subMenuElm);
	}

(Re-)set the timer for automatically closing the menu.

	checkNavTimer();

Close the function and release used memory.

	return false;
}

Explanation of the checkNavTimer function

Function and explanation taken from Quirksmode.org (with only minimal adjustment):

A special function for closing the entire navigation if the user does nothing for 5 seconds.

Each time the user mouses over any menu link the function checkNavTimer() is called. This function sets a timeout that calls closeAll() 5 seconds later.

function checkNavTimer() {

This function uses the variable navTimer in which the timeout is stored. First of all we see if navTimer is filled with a previous timeout from a previous mouseover event. If it is, we cancel the timeout. After all the user is still active so the menus shouldn't be closed.

	if (navTimer) clearTimeout(navTimer);

Then we set the timeout once again. After 5 seconds, we call closeAll(0) which closes all submenu's. The navigation is now completely closed.

	navTimer = setTimeout('closeAll(0)',5000);
}

Of course, if the user mouses over another link this timeout is canceled and a new one is set. The net result is that all layers close 5 seconds after the last mouseover event.

Explanation of the closeAll function

Function and explanation partially taken from Quirksmode.org:

When you close a submenu', you have to also close all submenu's below the current one.

This function is called from the main function navHoverStyle() and from the special function checkNavTimer(). Each time a level is passed to the function as argument. The submenu on this level and the submenu's on all lower levels should be closed.

function closeAll(lvl) {

We first set a variable which holds the number of currently open submenu' (minus 1 as the array starts at 0).

We set it here as we will remove items from the openMenus array in the for loop. Setting it in the for loop would cause memory drain as javascript would recalculate the variable for each loop.

	var oMl = openMenus.length-1;

We go through the submenu's that should be closed in reverse order. We start at the submenu' level with the highest number (= lowest level) and go back in the navigation to the submenu' level that's been passed to the function.

	for ( var i=oMl;i>=lvl;i--) {

We previously gave an anchor associated with a submenu we folded out the class unfolded.
So now when we close the submenu, we have to reverse that effect, by emptying out the className.
As the associated anchor is the element just before and on the same level as the submenu UL, we get the previousSibling of the current submenu to close and unset the className.

		var linkElm = openMenus[i].previousSibling;
		while (linkElm.nodeName != 'A')
			linkElm = linkElm.previousSibling;

		linkElm.className = '';

We set the display style of the submenu to be closed to none.

		openMenus[i].style.display = 'none';

After that we remove this - now closed - submenu from the openMenus array as the submenu is no longer open.

		openMenus.pop();

Then on to the next level until we reach the level that has been passed to the function.

	}
}

Browser compliancy

I have not exhaustively tested this menu in all possible browsers (or on all possible platforms), but I have tested it in some of the most commonly used browsers of this day and age.

Modern browsers on Windows

Older browsers on Windows

If you have tested this menu in another browser (and it works), please let me know and I will add that browser to the list.

Known bugs as of June 2nd 2006

None so far except for the ones mentioned in the browser compliancy list.

Contact me:

For bug reports, browser compliance reports or raving enthousiastic thank-you mails, you can contact me by e-mail: