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
23 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 (cssplay.co.uk)? They're excellent, free and webstandard ;-)
Comment by Sebastiaan – May 19, 2008
I do wonder about the
<quote>
Their second requirement was that the menu would stay expanded if the visitor clicked on one of those sub pages.
</quote>
how did you do *this* part? AJAX? Although easy enough with AJAX, I can't think of how to make it degrade gracefully, short of having 'regular' hrefs which are stripped of their target by a script onLoad and the Ajax built from them.
Your thoughts would be really appriciated!
Comment by Evan – October 30, 2008
On this project I used ColdFusion to add a class to the clicked link. When the page loads jQuery iterates through and hides the lists without an "a" tag with the class of "current" something like this:
$('#nav>li ul.subMenu').each(function(){
if ($(this).find('li a.current').size()==0) {
// current page is not in this menu list so hide
$(this).hide();
}
});
You could make this simpler with the parent() method, but I do some other stuff in the loop.
In the past I've used a cookie to record which link was clicked and then used a similar technique to achieve the same thing. jQuery has some cool cookie plugins.
Does that answer your question?
Comment by John Whish – October 31, 2008
I am trying your code but somehow i can't close the already opened sub menus
bldd.nl/jsproblems/convertToJQuery.html
In the end i want the submenu's slideIn from the left but first i like to fix this one.
Any pointers???
regards
Comment by alex – February 28, 2009
$(function(){
$('#nav>li').click(
function(){
$heading = $(this);
if ( $('ul:visible', $heading).size() == 0 ) {
$heading.siblings().find('ul:visible').slideUp(500);
$('ul', $heading).slideDown(500);
}
return false;
}
)
});
Comment by John Whish – February 28, 2009
For thinking along, i forgot to mention it only occurs in Firefox (mine is 3.x). In IE the menu works as aspected.
Anything i am overlooking for Firefox??
Comment by alex – March 03, 2009
Comment by John Whish – March 03, 2009
Thanks for any thoughts on this.
<ul id="nav">
<li>Heading 1
<ul>
<li>Sub page A</li>
<ul>
<li>Sub page 1</li>
</ul>
<li>Sub page B</li>
<li>Sub page C</li>
</ul>
</li>
Comment by Anthony Creek – April 27, 2009
I would have thought that the a filter like ul>li:hidden would do the trick.
Comment by John Whish – April 28, 2009
Comment by Anthony Creek – April 29, 2009
www.filamentgroup.com/examples/menus/flyout.php
I think it does what you want and looks great as well!
Comment by John Whish – April 30, 2009
I've got it working correctly in FF, but not IE. I've got it setup so the selected pages stay open by adding a class "selected" then the regular class name and calling to it in my javascript file. We'll see how it goes... I'll be working on it again this weekend.
Comment by Anthony Creek – May 01, 2009
I opted to use your "sea sick" example because in this case I have only one menu item with a sub-list. Looks great!
Comment by JackJames – May 13, 2009
Comment by John Whish – May 13, 2009
Comment by dave – May 14, 2009
Comment by John Whish – May 15, 2009
Comment by Dominik – August 14, 2009
I got this to work using this html/php
<ul id="nav">
<?php wp_list_pages('include=2,3,5,7&sort_column=menu_order&title_li='); ?>
</ul>
and the javascript from above.
But I want the current page's menu to stay expanded. I tried the suggestion from John Whish:
$('#nav>li ul.subMenu').each(function(){
if ($(this).find('li a.current').size()==0) {
// current page is not in this menu list so hide
$(this).hide();
}
});
but used li a.current_page_item because that's what wordpress names the class... but I'm having trouble with ul.subMenu because I don't have a way set a class to the submenus created by wp_list_pages.. Anyone know how to get around this:
It can be found here
stephenemlund.com/bridalshoppe/?page_id=2
Comment by Stephen Emlund – October 27, 2009
You should be able to just use simple selectors to find child elements. something like $('#nav>li>ul') which means direct decendant.
Comment by John Whish – October 27, 2009