Ryan Kinal
100 E. St. Clair St.
Warren, PA 16365
716.581.1000
ryan.kinal@gmail.com

Google ProfileTwitter: @IndigloMouth
FacebookLinkedIn

jQuery slideDown Menus

jQuery Slide-Down Menus

I'm sure there are many many many jQuery libraries that quickly and easily allow you to make slide-down menus. I know this, and yet I don't use them. I prefer to write my own, on a case-by-case basis. Every page is different, so every page needs a slightly different menu. I could be justifying my want (need?) to write my own code, rather than let other people do that for me or... well... I guess that's probably it. But the truth is that I enjoy writing my own code, and it sometimes beats the hassle of finding the right library for your needs. I used to write all my own effects, and I was kind of a stickler about it, but then I found jQuery, and it made writing slick effects so much easier.

No doubt, a lot of you already know about jQuery, and how to effectively use it. However, up until recently, I struggled with getting my slide-down menus (or even just show/hide menus) to work properly, and be properly positioned. When writing menus, I like to use very semantic code, as such:

<div id="nav"> <ul> <li><a href="#">Top Level Item</a><li> <li> <a href="#">Top Level Item</a> <ul> <a href="#">Second Level Item</a> <a href="#">Second Level Item</a> <a href="#">Second Level Item</a> </ul> <li> <li> <a href="#">Top Level Item</a> <ul> <a href="#">Second Level Item</a> <a href="#">Second Level Item</a> <a href="#">Second Level Item</a> </ul> <li> <li><a href="#">Top Level Item</a><li> </ul> </div>

With the following basic CSS:

#nav ul { margin: 0; padding: 0; list-style-type: none; } #nav ul li { margin: 0; float: left; } #nav ul li a { display: block; padding: 3px 5px; } #nav ul li ul { display: none; } #nav ul li ul li { float: none; }

A couple explanations: padding: 3px 5px gives a 3px padding on the top, and a 5px padding on the sides. This is not necessary, but it's a pretty standard value that I use, and it looks nice. float: left; is used on the top-level list items instead of display: inline; because some of them contain a second-level menu <ul>, which is by default display: block;, and an inline-level item cannot contain a block-level item. Some browsers will allow it, but others won't. Chrome even tries to correct it. So, use float: left; or float: right; instead.

I rarely use a menu more than two levels deep. If you want to, go somewhere else. (I'm sure the techniques I present can be modified to work, but I won't hand it to you. And yes, I know, display: none; is a no-no for accessability, but it's what I do anyway, for some reason or another.

Anyway, the problems I faced, in no particular order:

Alright, we'll address the last one first: Positioning Breaks. This is a very easy fix. All you have to do is give the second-level menus position: absolute. With the above HTML, it's as simple as the following:

#nav { position: absolute; }

Now, the second-level menu is pulled out of the flow of the page, and when it appears, it won't break the display. Awesome. Just remember to use margins to position the element further, and not left and top, as those properties are relative to the window, not the parent element (at least, in this case. If the parent element is also position: absolute;, then it is).

This leads us to the first problem: Disappearing Menus. The solution to this problem is not very obvious without backgrounds on the list items, and the second-level menus, and it won't actually occur unless the second-level menus have been positioned. There, of course, lies the problem: If the second-level menu is positioned too far from its parent element, there will be a gap, where a mouseout (or mouseleave, as we'll soon see) even will fire, causing the menu to be hidden. Semantically, this doesn't make much sense; The second-level menu is within the top-level menu item, so why doesn't the item expand to contain it? It seems like it should, and thus allow us to browse the menu without leaving the top-level item.

Ah, but we've taken the second-level item out of the flow of the page with position: absolute;. Problem number three caused problem number one! Tricky. So, even if it's just to test, put some backgrounds in your items and second-level lists, and see what they look like. If there's any gap at all, close it up, as it will save you a bunch of trouble.

And now, there's one last problem to take care of: Effect Doubling. It's tricky - it really is. Sort of. Maybe I was just tricked by it. The first couple times I ran into this problem, I gave up and went back to a simple show/hide, where it didn't happen (or, perhaps, was just imperceptible). But then I figured it out. The mouseover event was firing for the <li>, and then firing for <a> and bubbling back up to the <li>! Well, all I had to do was stop the bubbling, right? Right. But there isn't really a jQuery way of doing that. I mean, there probably is, but I found a better solution: Instead of mouseover, I'd use mouseenter. And instead of mouseout, I'd use mouseleave. These events don't bubble, and so I no longer had a double slideDown effect going on.

Not half bad! And, really, there wasn't much code that went into it. Here's the jQuery:

$(function() { $('#nav > ul > li').mouseenter(function() { $(this).find('ul').slideDown('fast'); }).mouseleave(function() { $(this).find('ul').slideUp('fast'); }); });

And here's the CSS:

#nav ul { list-style-type: none; padding: 0; margin: 0; } #nav ul li { float: left; padding: 3px 5px; } #nav ul li ul { display: none; position: absolute; margin: 3px 0 0 -5px; padding: 5px 15px; list-style-type: none; } #nav ul li ul li { float: none; padding: 1px 3px; } #nav ul li ul li a:hover { color: #ccc; }

All in al, that's seven lines of jQuery and five of CSS (if you do inline property declarations like I do). Of course, this doesn't include any of the frills, like font sizes, background colors, hover styles, or anything like that. The version I did this weekend actually uses the JavaScript to set background colors for the top-level list items on enter, and take them away as a callback when they're done sliding back up. It's pretty slick, I'd say. Not perfect, as with the right mouse movements, you can end up with a menu that is shown, and a top-level list item without a background color, but hey, nothing's perfect.

Also, if you want to increase the accessibility of your page, it's fairly easy to position the second-level menus off of the screen instead of hiding them. It would probably go something like this:

$(function() { $('#nav > ul > li').mouseenter(function() { $(this).find('ul').hide().css('margin-left', '0').slideDown(); }).mouseleave(function() { $(this).find('ul').slideUp('fast', function() { $(this).css('margin-left', '-999px').show(); }); }); });

Make sure the sub-menu is hidden before you move it into view to slide it down, and you have to use the callback function of slideUp() to make sure the menu isn't moved off the screen before it's re-shown for accessability purposes. Note that this is untested, but it looks like it should work, doesn't it?

says:
My counter-proposal, for accessibility:

via server side (php or whatever), the css starts with all the submenus display:none except an &quot;active&quot; submenu, which displays:block, active being whichever dropdown section contains the current page.
So all you'd have to do is add the capability for a current dropdown:
#nav ul li #active-dropdown { display: block; }

That means that even without any js, or if the js breaks, you can still access all of the dropdown menus (simply by browsing to each top-level link in turn, you could get access to the second level items).

Now you can hide the active on ready, and do all the javascript stuff, without having to worry about the failure conditions. Hell, when it breaks in ie6 (like it always does), you can just browser select and skip the active javascript for ie6. I know I was happy to be able to do so.
Err, that was Roy again. says:
I feel like a stalker, always being th' one to post on your blog here. :p
Name:
Are you human? (enter yes if so)
Comment: