Pure CSS-based drop-down menus are a great thing, if for no other reason than their sheer simplicity and flexibility. However, they have two main drawbacks.

  1. They don’t work in Internet Explorer 6 due to the browser’s poor support for the :hover pseudo-class.
  2. When the mouse cursor is over a drop-down, the top level navigation item does not stay highlighted under most conditions.

Fortunately, both problems can be solved with some simple JavaScript. The first problem is easily corrected with the excellent Son of SuckerFish drop-down code. The second problem can be solved using the equally small amount of code described below.

The best part of this code its use of DOM hooks in your XHTML document to add the functionality making it self-contained and allowing full separation of content and design.

For the sake of simplicity, and reducing the number of called functions, I’ve combined the SuckerFish JavaScript with my own.

How this works

The code is pretty simple, but for novice JavaScriptors (such as myself), here is a set-by-step guide on how the function works.

Internet Explorer 6 Functionality: The SuckerFish Code

First, we declare the function and add the SuckerFish code for Internet Explorer 6 compatibility. Since this functionality is already described in their article, I’ll just note two small changes I’ve made.

  • The original SuckerFish code had the ID hard-coded into the function. The below version has a variable instead of the hard-coded ID, allowing us to use the function on several different drop-downs.
  • I’ve corrected an error in the regular expression which was causing the class name to remain on the list element when the user moves their cursor off the drop-down (this error and the fix used here was originally pointed out by Tim Gaunt).
menuHover = function(nav) {
  var sfEls = document.getElementById(nav).getElementsByTagName("li");
  for (var i=0; i<sfEls.length; i++) {
    sfEls[i].onmouseover=function() {
      this.className+=" sfhover";
    }
    sfEls[i].onmouseout=function() {
      this.className=this.className.replace(new RegExp("\\s?sfhover\\b"), "");
    }
  }

Code for persistent top-level menu formatting

Now we get into the code that maintains the hover state for top-level navigation elements when the user has moved the cursor over a drop-down list. Fundamentally, this works the same way as the SuckerFish function, but with some adjustments. The system cycles through each drop-down <ul> and adds a function that tells the its parent anchor tag to take on a particular class when the browser detects a mouseover on the menu.

First, we create a variable selecting each sub-navigation list by using the main <ul> ID. This will allow us to add a mouseover function to each of the drop-downs.

var listItem = document.getElementById(nav).getElementsByTagName('ul');

Next, we cycle through each of the <ul> drop-downs so we can add a function on each one.

for(var i=0;i&lt;listItem.length;i++) {

If the browser detects the mouse cursor over the current drop-down it will pull into an array all anchors found in the parentNode (which is, basically, whatever the parent element is in the DOM, in this case the parent node is a <ul>). It then assigns the first anchor, presumably the anchor in the top-level navigation, a new class name. In this case, the class name “anchor”.

listItem[i].onmouseover=function() {
  var changeStyle = this.parentNode.getElementsByTagName('a');
  changeStyle[0].className+=" active";
}

Now, when the user moves the cursor away, we simply remove the class name and the anchor is restored to its original style.

listItem[i].onmouseout=function() {
  var changeStyle = this.parentNode.getElementsByTagName('a');
  changeStyle[0].className=this.className.replace(new RegExp("\\s?active\\b"), "");
}

Playing well with others: onLoad Function

Now we need to call our function on page load so the menus will work from the start. To do this we’ll use John Resig’s addEvent on load function. This is useful for two reasons:

  1. Using this function will allow us to add as many on load events as we like (or what the browser will handle) without worrying that the various functions will interfere with each other.
  2. John’s function helps avoid the Internet Explorer caching problem that can cause performance problems for end users.

Here is the code:

function addEvent( obj, type, fn ) {
  if ( obj.attachEvent ) {
    obj['e'+type+fn] = fn;
    obj[type+fn] = function(){obj['e'+type+fn]( window.event );}
    obj.attachEvent( 'on'+type, obj[type+fn] );
  } else
    obj.addEventListener( type, fn, false );
  }
function removeEvent( obj, type, fn ) {
  if ( obj.detachEvent ) {
  obj.detachEvent( 'on'+type, obj[type+fn] );
  obj[type+fn] = null;
  } else
    obj.removeEventListener( type, fn, false );
  }

addEvent(window, 'load', function () { menuHover('menu'); });

Take a look at John Resig’s post “Flexible Javascript Events” on this function to get a better idea how it works.

The CSS and XHTML

Last but not least, we need our CSS and XHTML on which we’ll apply our JavaScript. The following is almost exactly like the SuckerFish code so I will note the one, very small, different.

In order to apply the highlighted state to the top-level navigation anchor, we need to give it a class name. Fortunately, since we already have a style declaration for the hover state, we’ll just append another declaration name to the original style, as you can see below.

#menu li.dropdown a:hover, #menu li.dropdown a.active {
  color: white;
  background: #999 url(../i/arrow_down.gif) no-repeat right 50%;
}

Areas for improvement

  1. This will only work if your top-level navigation style is on an anchor tag. If your main style is on the <li> then the JavaScript won’t select it in order to add the custom class. A future improvement might include passing a variable with the tag or class the function should reference.
  2. The class name JavaScript references is hard coded into the function. Fortunately the script will pull the class relative to the element in the DOM, so you can have two classes with the same name with different styles so long as they are declared on different elements higher in the cascade.To illustrate, if using the following code:
    <ul id="menu">
    	<li><a class="active" href="#">Link</a></li>
    </ul>

    This function will apply the style #menu ul li .active and not the style #content ul li .active.

    Nonetheless, passing a variable with the highlighted class name to the function would help make this function truly flexible.

Complete Code

<script type="text/javascript">// <![CDATA[
menuHover = function(nav) {
    var sfEls = document.getElementById(nav).getElementsByTagName("li");
    for (var i=0; i<sfEls.length; i++) {
      sfEls[i].onmouseover=function() {
        this.className+=" sfhover";
      }
      sfEls[i].onmouseout=function() {
        this.className=this.className.replace(new RegExp("\\s?sfhover\\b"), "");
      }
    }

    var listItem = document.getElementById(nav).getElementsByTagName('ul');
    for(var i=0;i			<listItem.length;i++) {
      listItem[i].onmouseover=function() {
        var changeStyle = this.parentNode.getElementsByTagName('a');
        changeStyle[0].className+=" active";
      }

      listItem[i].onmouseout=function() {
        var changeStyle = this.parentNode.getElementsByTagName('a');
        changeStyle[0].className=this.className.replace(new RegExp("\\s?active\\b"), "");
      }
    }
  }

  function addEvent( obj, type, fn ) {
    if ( obj.attachEvent ) {
      obj['e'+type+fn] = fn;
      obj[type+fn] = function(){obj['e'+type+fn]( window.event );}
      obj.attachEvent( 'on'+type, obj[type+fn] );
    } else
      obj.addEventListener( type, fn, false );
    }
  function removeEvent( obj, type, fn ) {
    if ( obj.detachEvent ) {
    obj.detachEvent( 'on'+type, obj[type+fn] );
    obj[type+fn] = null;
    } else
      obj.removeEventListener( type, fn, false );
    }

  addEvent(window, 'load', function () { menuHover('menu'); });
// ]]></script>
<ul id="menu">
	<li><a href="#">Styles</a>
<ul>
	<li><a href="#">Red/White</a></li>
	<li><a href="#">Sparkling</a></li>
	<li><a href="#">Dessert</a></li>
	<li><a href="#">Fortified</a></li>
	<li><a href="#">Fruit</a></li>
	<li><a href="#">Ice Wine</a></li>
</ul>
</li>
	<li><a href="#">Whites</a>
<ul>
	<li><a href="#">Chardonnay</a></li>
	<li><a href="#">Chenin blanc</a></li>
	<li><a href="#">Muscat</a></li>
	<li><a href="#">Pinot blanc</a></li>
	<li><a href="#">Pinot gris</a></li>
	<li><a href="#">Riesling</a></li>
	<li><a href="#">Sauvignon blanc </a></li>
	<li><a href="#">S?©millon</a></li>
</ul>
</li>
	<li><a href="#">Reds</a>
<ul>
	<li><a href="#">Cabernet Sauvignon </a></li>
	<li><a href="#">Malbec </a></li>
	<li><a href="#">Merlot </a></li>
	<li><a href="#">Pinot noir </a></li>
	<li><a href="#">Syrah/Shiraz </a></li>
	<li><a href="#">Zinfandel</a></li>
</ul>
</li>
	<li><a href="#">Noted Regionals</a>
<ul>
	<li><a href="#">Amarone</a></li>
	<li><a href="#">Beaujolais</a></li>
	<li><a href="#">Burgundy </a></li>
	<li><a href="#">Chianti </a></li>
	<li><a href="#">Madeira </a></li>
	<li><a href="#">Port </a></li>
	<li><a href="#">Sancerre </a></li>
	<li><a href="#">Tokaji </a></li>
	<li><a href="#">Vinho Verde </a></li>
</ul>
</li>
	<li><a href="#">Key Countries</a>
<ul>
	<li><a href="#">France</a></li>
	<li><a href="#">Italy</a></li>
	<li><a href="#">Spain</a></li>
	<li><a href="#">United States</a></li>
	<li><a href="#">Argentina</a></li>
	<li><a href="#">Australia</a></li>
	<li><a href="#">South Africa</a></li>
</ul>
</li>
</ul>

Update: 6 September 2009

I recently got an email from a reader noting how IE6 caused a flicker when mousing over the top level navigation and inquiring if there was a way to fix this. Fortunately, the fix is quite easy. Simply add “position: relative” to the CSS declaration “#menu li a“.

Basically, what is happening is IE6 hovers don’t work very well when hovering over padding on an element. So the hover state disappears when moving from the actual text to the padding area and then to the menu below. The “position: relative” trick is useful for solving this in many other situations.

I’ve updated the code above to reflect this change.

This post has 23 Comments

23
  1. hello, my name is Liran and i live in ISRAEL
    i like to use some of your scripts (drop down menu. & the icons script)
    are those freeware/GNU licence? can i use them?

  2. Of course, feel free to use any of the code here!

    If there are any citations, please leave those. Not so much for my own sake, but in case I referenced someone else’s code, it is best they get due credit.

  3. My friend, this is excellent and so clean. In Safari 3.1 the top level navigational item doesn’t stay highlighted which is a shame, but for the rest of it it’s the best I’ve found.

    Could you do more testing on browsers and possibly post some fixes? If I had the skill to do this I would! Thanks again mate, you have really helped me out :)

  4. Hello, back again. Would you post some more examples like this? Not of navigation menus, but of other CSS bits ‘n’ bobs? Cheeky? Yes, I know, but you’re good lol!

  5. i have a similar css menu setup but my top level (parent) are images. That is, .

    I have rollover classes for the images, but how do I keep the the top-level (parent) highlighted?

    I tried messing with your code and if you give each of the top level anchors an unique class name, it doesn’t stay highlighted?

    That would make it ‘a.classname.active’, correct? How can I make it a.classname:active, and have it work?

    Thanks
    Nate

  6. Hi Nate,

    Have a link? I’d be happy to take a look at the code.

    You could have a single image for the default, hover and active states and then merely adjust the positioning to have it display differently in different modes. The code would probably look something like this:

    <ul>
      <li class="home"><a href="#" rel="nofollow">Home</a></li>
      <li class="about"><a href="#" rel="nofollow">About</a></li>
    </ul>
    

    And then for the CSS, something like this:

    ul a { background-position: top left; }
    ul a.home { background-image: url(home.png); }
    ul a.about { background-image: url(about.png); }
    
    ul a:hover,
    ul a.active { background-position: left -20px; }
    

    Makes sense? Let me know if this could use some clarification.

  7. That actually makes a lot of sense. Thank you for this suggestion, it has helped me solve the same scenario (top level are images).

  8. Hey man,

    Thanks for this fantastic and clean code. I am having a problem with the multiple level menu as outlined in the Son of Suckerfish example. The second level simply doesn’t appear. Is there something I am missing which makes your code unable to function on more than one level?

    Cheers,
    Ed

  9. Let me keep the comments fresh and add another THANKS!
    The article was another big help and then the comment response to Nate sealed the deal.

    One question:
    I noticed on IE 7 that the dropdown menus would stay on sometimes if I moved the cursor too quickly. Can you tell me where I should look for issues like that? Tutorial or link? I’m a novice with Javascript and not sure if that was the culprit in relation to CSS or something. Oddly, once I applied my need for images in the parent UL the issue didn’t happen anymore.

    Thanks again. Bookmarked and shared.

  10. I have been trying to get my site to have working drop down menus for months. I finally managed to get them working but I am still having the same problem you where having. I tried out your script but it doesn’t seem to do anything. I know nothing about JS. I was wondering if you could help me out.

  11. Hi, this is exactly what I was looking for, but I have one issue that seems to be happening on all browsers I’ve tested. If I click on one of the drop-down links and go to that page; when I hit ‘back’ in my browser, it goes back to the page I came from, but the drop-down is stuck as if I had my mouse over it. Nothing seems to disable the mouse-over affect unless I refresh the page.

    Is there something I can put at the top of my html code that will force the javascript to clear any menuhover() function calls? I tried to do something like this already, but had no luck.

    Any help would be great since I’ve already employed this menu structure into my project and would love to continue using it without a great deal of backtracking. Thanks!

  12. I have a slight correction to my above comment/question. The issue seems to be only happening in Firefox. IE and Chrome both work fine as of right now.

  13. Hello everyone,

    Sorry about taking so long to reply, been rather swamped with work of late!

    @DeviousMrBlonde: Are you looking at the menu in IE6/7? If so, I imagine you’d have to add more code to activate the menu in the lower LI tags, similar to what is done for the top-level elements.

    @dbone: I’m not sure exactly how to address the problem you’re seeing in IE7. It could be that the overstate is not being removed when moving the mouse quickly to another top-level element. Perhaps adding some spacing in the CSS between the menu elements? This way the mouse cursor would be over an inactive area, perhaps giving IE7 enough time to remove the hover state.

    @banks: Have a link I can take a look at?

    @Demetri: My guess is Firefox is pulling a cached version of the page, which would include the “active” class name. I possible solution would be to clear all “active” class names on page load.

    The Javascript would look something like this (note, I haven’t tested this, so it may not work):

    var listItem = document.getElementById(nav).getElementsByTagName('ul');
    for(var i=0;i<listItem.length;i++) {
        var changeStyle = listItem[i].parentNode.getElementsByTagName('a');
        changeStyle[0].className=listItem[i].className.replace(new RegExp("\\s?active\\b"), "");
    }

    You'd put this somewhere in the "menuHover()" function. Let me know if this does or doesn't work!

  14. Darren,

    You’re right, it does pull the cached version of the page when I hit back. I’ve tried your code, but I still get the same result. The problem seems to be that the page doesn’t actually execute any of the javascript on the page (whether it be from an included .js file or within script tags in the page’s code) when it displays this cached page.

    … as I was writing this, I thought of adding the function call to menuHover() when the page unloads, so it would be forced to ‘deactivate’ the active classes. This apparently worked.

    For those who are interested, I just added this to my body tag:
    onunload=”javascript: menuHover();”

    Along with adding the code Darren provided above, to the menuHover() function.

  15. Darren,

    I’m a bit new to css/design, and I am attempting to implement your solution here, but I’ve run into a problem that I can’t seem to fix. I have my menu set up, with a div containing the entire menu and submenu, and everything is labeled from that, as opposed to how you have your main labeled with an id. Everything I have looks fine like I want it except for some reason the very first of the main menu stays highlighted no matter which other list item I am on. However, all of the other list items in the main menu work as they should, only becoming highlighted when I am hovering over them or in their submenu. Any ideas?… I can send you my styling and html in a txt file if you think that would help.

  16. I’m having an issue with the first menu item the same issue as John. Was there a solution for this?

  17. In my case i have added image for a and a:hover. Menu is smoothly running in all browser except ie6!
    In ie6 menu is flickering by hover. Is there any solution for this?

  18. Hi

    As above I’m having two issues:

    1. In all browsers, the first list item always takes its hover state no matter which item in the list you hover over.

    2. I’m gettiing a nasty flicker in IE6 which won’t seem to go away, even with the solution highlighted above.

    Anyone have a solution?

  19. I created a site with dropdown menus…….on the top and when i view in morzilla firefox…..it works perfectly fine and it stay on the top but when i view the same thing on internet explore the menus are on the right side and drop down are on the left side ……………

    help

  20. Hi,
    this is a really great tutorial. Thanks a lot for making it. I just have one question. Is it possible to keep the menu expanded and one of the buttons in the menu in active state even when the mouse in’t over any of the navigation. Help would be really appreciated.
    Thanks a lot
    Ciaran

  21. @Ganybhat and @JJ Check out the “position: relative” for the IE6 hover issue.

    @Ganybhat, @JJ and @Hi: Do you have code I could take a look at? Quite likely you have some other CSS interfering with the menu.

    @Ciaran: Sure, this would be possible. Do you have a particular use in mind? The reason I ask is that you may be better with a menu that is activated by click rather than the hover state. The hover JS is only needed for IE6, so the CSS is what handles the drop down menu.

    To change this functionality, you’d have to add some more JS to override the default “:hover” action on mouseOut.

    Let me know what you’re aiming for and I can suggest a better solution.

    Cheers,
    Darren

Leave a Reply to Liran Shapira Cancel reply

Your email address will not be published. Required fields are marked *