Expanding menus with jQuery
May 13, 2008
I was recently asked by a client to do a menu which would expand to show pages when the visitor moved their mouse over the menu heading to show sub pages of that section. Their second requirement was that the menu would stay expanded if the visitor clicked on one of those sub pages.
The code for the menu was a simple list with nested lists inside for the sub pages. For example:
<ul id="nav"> <li>Heading 1 <ul> <li>Sub page A</li> <li>Sub page B</li> <li>Sub page C</li> </ul> </li> <li>Heading 2 <ul> <li>Sub page D</li> <li>Sub page E</li> <li>Sub page F</li> </ul> </li> <li>Heading 3 <ul> <li>Sub page G</li> <li>Sub page H</li> <li>Sub page I</li> </ul> </li> </ul>
Please note that I have removed the "a" tags for ease of reading.
This presented a few problems; the first was how the collapse the menu for visitors who have JavaScript (and stay expanded for visitors without for accessibility reasons). This is simple with jQuery:
$(function(){
$('#nav>li>ul').hide();
});
Note the use of the ">" to make sure that the function is only called on direct descendants of the id of nav.
The second problem was to make the menu expand when the visitor pointed at a heading.
$(function(){
$('#nav>li>ul').hide();
$('#nav>li').mouseover(function(){
// open current menu if it's closed
$(this).find('ul:hidden').slideDown(500);
})
});
So far so good. Now to get the menu to collapse when the visitor moves their mouse over another heading.
$(function(){
$('#nav>li>ul').hide();
$('#nav>li').mouseover(function(){
// close open sub menus
$(this).siblings().find('ul:visible').slideUp(500);
// open current menu if it's closed
$(this).find('ul:hidden').slideDown(500);
})
});
This works but can make you feel a bit sea-sick so we can improve on it by waiting for the submenus to close before opening the one we want by using the callback functionality of the slideUp method.
$(function(){
$('#nav>li>ul').hide();
$('#nav>li').mouseover(function(){
// create a reference to the active element (this)
// so we don't have to keep creating a jQuery object
$heading = $(this);
// close open sub menus
$heading.siblings().find('ul:visible').slideUp(500, function(){
// open current menu if it's closed
$heading.find('ul:hidden').slideDown(500);
});
})
});
The eagle eyed will spot that this won't work. Why? Because if none of the submenus are expanded then then callback will never be fired as no menus will slide up. To get round this we need to do this:
$(function(){
$('#nav>li>ul').hide();
$('#nav>li').mouseover(function(){
// create a reference to the active element (this)
// so we don't have to keep creating a jQuery object
$heading = $(this);
// check to see if any sub menus are open
if ($heading.siblings().find('ul:visible').size()!=0) {
// close open sub menus
$heading.siblings().find('ul:visible').slideUp(500, function(){
// open current menu if it's closed
$heading.find('ul:hidden').slideDown(500);
});
}
else {
// open current menu if it's closed
$heading.find('ul:hidden').slideDown(500);
}
})
});
This all works nicely except when you open the sub menu of Heading 1, then try to open the Heading 2 submenu. The problem is that when the menu contracts, it moves the headings under the mouse firing the mouseover event. So if you try and point at Heading 2, the chances are that you'll end up with Heading 3 expanded.
To stop this happening, jQuery has a handy filter called "animated" which checks to see if an element is currently being animated. We can use this to disable the mouseover event. Our final code looks like this.
The JavaScript file (to include in your HTML head tag)
$(function(){
$('#nav>li>ul').hide();
$('#nav>li').mouseover(function(){
// check that the menu is not currently animated
if ($('#nav ul:animated').size() == 0) {
// create a reference to the active element (this)
// so we don't have to keep creating a jQuery object
$heading = $(this);
// create a reference to visible sibling elements
// so we don't have to keep creating a jQuery object
$expandedSiblings = $heading.siblings().find('ul:visible');
if ($expandedSiblings.size() > 0) {
$expandedSiblings.slideUp(500, function(){
$heading.find('ul').slideDown(500);
});
}
else {
$heading.find('ul').slideDown(1000);
}
}
});
});
The HTML menu
<ul id="nav"> <li>Heading 1 <ul> <li>Sub page A</li> <li>Sub page B</li> <li>Sub page C</li> </ul> </li> <li>Heading 2 <ul> <li>Sub page D</li> <li>Sub page E</li> <li>Sub page F</li> </ul> </li> <li>Heading 3 <ul> <li>Sub page G</li> <li>Sub page H</li> <li>Sub page I</li> </ul> </li> </ul>
- Posted in:
- jQuery
5 comments
Leave a comment
If you found this post useful, interesting or just plain wrong, let me know - I like feedback :)

When I include it in a source page in a cflayoutarea the menu opens expanded.
Can you think of what I am doing wrong? I am using the same code except that I am placing the scripts in a cfsavecontent block and then returning that value in a cfhtmlhead.
Comment by Michael Brennan-white – May 13, 2008
Comment by John Whish – May 13, 2008
That worked in Firefox very well but the slideup method in IE7 is very jerky even when I run the code on a page outside the RIA.
I personally like the idea of punishing IE users but this is not a way to win over Hearts and minds.
Comment by Michael Brennan-white – May 13, 2008
if ($expandedSiblings.size() > 0) {
$expandedSiblings.hide(500, function(){
$heading.find('ul').show(500);
});
}
else {
$heading.find('ul').show(1000);
}
or the fade method which works well
if ($expandedSiblings.size() > 0) {
$expandedSiblings.fadeOut(500, function(){
$heading.find('ul').fadeIn(500);
});
}
else {
$heading.find('ul').fadeIn(1000);
}
Comment by John Whish – May 13, 2008
have you tried out one of the CSS menu's by CSS Play (http://cssplay.co.uk)? They're excellent, free and webstandard ;-)
Comment by Sebastiaan – May 19, 2008