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

Leave a reply

Enter the text you see here

saving...