Creating Delayed Drop-Down Menus in Jquery without Losing Accessibility
Posted by ryan in accessibility, css, jquery
Making menus fun for everyone
Regardless of what you think of drop-down menus, you'll quite likely find yourself making one eventually. When you do, here are 2 important goals to keep in mind for success:
- Accessibility - make sure your menu works without javascript (I think a failure here is a cardinal sin)
- User-friendly - add a "delay" to your drop-down menu so that it doesn't appear/disappear if the user's mouse slips on/off the menu for just a moment (which is annoying)
The goal:
The best way to understand what we're going for is just to
try out the end result:
Things to notice are:
- You can turn javascript off, and the drop-down effect works (just without the delay).
- You can slip on/off of the drop-down menus quickly without them opening/closing. This is the delayed effect.
Prerequisites
To start rolling, you'll need both jQuery and its hoverIntent plugin. Because I like to use Google's hosted version of jQuery, my header includes the following.
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.2.6/jquery.min.js"></script> <script type="text/javascript" src="/path/to/jquery.hoverIntent.minified.js"></script>
The setup - power with just CSS
<div id="delayed-drops"> <div class="nav-item"> <a href="#">Good Stuff</a> <div class="drops"> <a href="#">Sig bottles</a> <a href="#">Lake Michigan</a> <a href="#">Blue Door Project</a> <a href="#">Waffles</a> </div> </div> <div class="nav-item"> <a href="#">Bad Stuff</a> <div class="drops"> <a href="#">Self-Employment Taxes</a> <a href="#">Plastic</a> </div> </div> </div>
// The CSS #delayed-drops .nav-item .drops { position: absolute; z-index: 100; display: none; background: #fff; } #delayed-drops .nav-item:hover .drops { display: block; }
With just the above code, we've got a working drop-down menu system powered by only CSS (i.e., it works with javascript off). This works in all browsers (except IE6, which requires a small bit of javascript), but doesn't include the nice delayed effect. We add that next.
Adding a delay - making friends
To make our menu friendlier, we're going to add a hover delay, which will allow the user to "slip" on/off of the menu for a moment without the menu abruptly opening/closing. We do this via jQuery's hoverIntent plugin. This plugin extends the idea of "onMouseOver" and "onMouseOff". With this plugin, each event is only fired if the user has hovered and has stayed hovered over something for some period of time. It's called hoverIntent because we want to be sure that hovering is what the user is intending to do. The plugin is very configurable and quite small.
We add the delay with the following code. The javascript adds a new CSS class "show" that will display our menus.
<script type="text/javascript"> $(document).ready(function(){ $("#delayed-drops .nav-item").hoverIntent({ interval: 150, // milliseconds delay before onMouseOver over: drops_show, timeout: 500, // milliseconds delay before onMouseOut out: drops_hide }); }); function drops_show(){ $(this).addClass('show'); } function drops_hide(){ $(this).removeClass('show'); } </script>
<style type="text/css"> #delayed-drops .nav-item.show .drops { display: block; } </style>
With this setup, the menu should drop down only when the user has been hovering over the menu head for 150 milliseconds. Similarly, the menu should disappear only when the user has been off of the menu for a full 500 milliseconds.
If you try it now, you'll get (nearly) the desired effect. With javascript off, our new javascript code is useless, but our original CSS engine picks up the slack and the menu system functions normally (just without our nice delay). With javascript on, our menus snap down instantly, but then allows for the user to slip off of the menu for up to 500 milliseconds without closing the menu (just as it should).
Not quite right...
The menu system should only snap down once the user has been hovering over the menu for 150 milliseconds (currently it happens instantly). Here's what's happening behind the scenes:
- The user's mouse hovers over the menu, which adds the :hover pseudo-class to .nav-item. This causes the menu to drop down immediately.
- 150 milliseconds later, our javascript fires (drops_show()) and adds the "show" class. However, since the menu is already being shown, this has no visible effect.
- When the user's mouse leaves the menu, the :hover pseudo-class is removed from .nav-item. The menu, however, does NOT disappear because the "show" class is still present.
- 500 milliseconds later, our javascript fires (drops_hide()) and removes the "show" class. Our drop-downs disappear.
The CSS and javascript engines act at the same time, getting in each other's way. Ideally, we want the smooth javascript funtionality while keeping keep the CSS functionality only as a backup for when javascript is disabled. To do this, we need to disable the CSS drop-down functionality when we're sure that javascript is enabled.
Use either CSS or Javascript, but not both
The key is to use javascript to add a new "with-js" class that disables the CSS drop-down menu behavior. Our full javascript and CSS now look like this:
<script type="text/javascript"> $(document).ready(function(){ $("#delayed-drops .nav-item").hoverIntent({ interval: 150, over: drops_show, timeout: 500, out: drops_hide }); $("#delayed-drops .nav-item").addClass('with-js'); }); function drops_show(){ $(this).addClass('show'); $(this).removeClass('with-js'); } function drops_hide(){ $(this).removeClass('show'); $(this).addClass('with-js'); } </script>
<style type="text/css"> #delayed-drops .nav-item .drops { position: absolute; z-index: 100; display: none; background: #fff; } #delayed-drops .nav-item:hover .drops, #delayed-drops .nav-item.show .drops { display: block; } #delayed-drops .nav-item.with-js .drops { display: none !important; } </style>
Here's how our completed system works:
- The page loads and javascript adds the .with-js class to all of the menu heads.
- When the user's mouse hovers over the menu, nothing visible happens. The :hover pseudo-class is added to .nav-item, but .drops isn't displayed because the .nav-item.with-js class overrides .nav-item:hover.
- 150 milliseconds later, the javascript fires and first adds the .show class. This would normally be enough to display the drop-down menu, but it too is overriden by the .nav-item.with-js class. To solve this, our javascript also removes the .with-js class. The end result is that our drop-down menu is displayed.
- When the user's mouse leaves the menu, nothing visible happens. The .nav-item:hover psuedo-class stops, but the .show class is still present, keeping our drop-down menu visible.
- 500 milliseconds later, the javascript fires and first removes the .show class. This causes the menu to disappear. The class .with-js is then readded to prevent the CSS :hover pseudo-class from prematurely displaying our menu system the next time around (see #1).
Summary
Creating a drop-down menu system with a user-friendly delay is easy with jQuery's hoverIntent plugin. Even more well-known today is the ease with which you can build a menu system that functions without the need for javascript (except in IE6). Combining the two, however, can create problems as both your CSS and javascript code attempt to power your menu system. The result is behavior that's a hybrid of the 2 methods. With the above trick, you can keep the 2 methods separate, leaving you with a functional delayed drop-down menu that works without javascript.
I'm having the same problem as Tim.
The code isn't working with nested ul's and li's. I'm probably making a mistake somewhere.
It would be really appreciated if u help sort out the code.
Thanks!
But, as others posted, I can only get this to work with DIV's - not ul's and li's as others have posted. Since UL/LI is the accepted way to make menus, I wonder if anyone has a fix?
ID of the menu is #mainlevelmenukibar. When mouse hover on a LI of this menu, it looks for the UL in this LI and it fades in if it finds. When mouse leaves the LI, it looks for the UL again and fades out it.
jQuery.noConflict();
jQuery(document).ready(function() {
/* Aç?l?r Yatay Menü için jQuery Kodlar? */
ac_kapa = {
sensitivity: 3, // number = sensitivity threshold (must be 1 or higher)
interval: 200, // number = milliseconds for onMouseOver polling interval
timeout: 600, // number = milliseconds delay before onMouseOut
over: function() {
jQuery(this).find('ul').fadeIn('fast');
jQuery(this).find("ul li ul").hide();
},
out: function(){jQuery(this).find("ul").fadeOut('fast');}
}
jQuery('#mainlevelmenukibar li').hoverIntent(ac_kapa)
})
Unfortunately, being an old post - the code itself is long gone! Sorry for the inconvenience!