02.08.09

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:

  1. Accessibility - make sure your menu works without javascript (I think a failure here is a cardinal sin)
  2. 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: drops.jpg
Things to notice are:

  1. You can turn javascript off, and the drop-down effect works (just without the delay).
  2. 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:

  1. 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.
  2. 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.
  3. 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.
  4. 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:

  1. The page loads and javascript adds the .with-js class to all of the menu heads.
  2. 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.
  3. 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.
  4. 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.
  5. 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.

Attachments

Simple and Advanced Todo Module (drops-example.tar.gz)
Thanks for the shares!
  • StumbleUpon
  • Sphinn
  • del.icio.us
  • Facebook
  • TwitThis
  • Google
  • Reddit
  • Digg
  • MisterWong
Posted by Haig Evans-Kavaldjian on 2010-01-26
This is great, but would be even better if it were usable without a mouse (that is, with a keyboard only)...
Posted by jason on 2010-02-18
attachment not found. please re-up!
Posted by Patrick Long on 2010-03-09
Great idea. Just the part I wanted to isolate from Suckerfish. I am having some prblems getting it to work on our 3 level nav though
Posted by Tim on 2011-01-30
This does exactly what i need, but can you apply it to nested ul's and li's instead of divs? I can't seem to get it to work!! Very much a beginner here... Any help would be much appreciated!! Cheers
Posted by fahad on 2011-03-15
Hi

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!
Posted by dever on 2011-06-20
This is exactly what I need - a delayed JS menu with a CSS fallback! Thank you!
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?
Posted by Jim Brisson on 2011-07-31
Wonderful explanation of exactly the function I wanted. Thanks so much.
Posted by Efe Co?an on 2011-08-10
Here is the fix for UL/LI menus.

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)


})
Posted by Chakir on 2011-12-02
Thank you.. You just saved my day
Posted by Mark on 2012-02-06
Hi, the link to the download is broken.
Posted by dai on 2012-02-19
you waste my 13sec in my life
Posted by Website Designer Kansas City on 2012-02-25
Not working very well, most people are prob using ul li for menu's, not div's.. The fix seems to have gotten decoded
Posted by Ryan on 2012-02-26
Yea, this is a pretty old post now - I think that using ul/li is a better way to go and that this same idea should be decoded.

Unfortunately, being an old post - the code itself is long gone! Sorry for the inconvenience!