CSS drop-down menus: how to keep the top level selected when hovering over a sub-menu
Posted 12 April 2007 Tagged to CSS, JavaScript, Web Development
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.
- They don’t work in Internet Explorer 6 due to the browser’s poor support for the
:hoverpseudo-class. - 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<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:
- 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.
- 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
- 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.
-
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 href="#" class="active">Link</a> </li> </ul>This function will apply the style
#menu ul li .activeand 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
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<title>Menu Example</title>
<style type="text/css" media="screen">
body {
margin: 20px;
background: #fff;
font-family: Verdana, Helvetica, sans-serif;
font-size: 9pt;
}
* { margin: 0; padding: 0; }
li { list-style: none; }
a { text-decoration: none; }
#menu li {
float: left;
display: block;
font-size: 8pt;
}
#menu li a {
padding: 6px 10px;
display: block;
font-weight: bold;
color: #333;
}
#menu li a:hover, #menu li a.active {
background: #999;
color: white;
}
#menu li ul {
left : -999em;
position : absolute;
}
#menu li ul li { float : none; }
#menu li ul a {
width : 136px;
padding : 8px 10px;
background : #f4f4f4;
border-bottom : 1px solid white;
font-weight : normal;
}
#menu li ul a:hover {
background: #FFCC99;
color: #333;
}
#menu li:hover ul, #menu li.sfhover ul {
left: auto;
border-top: 1px solid #999;
}
</style>
<script type="text/javascript"> <!--
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>
</head>
<body>
<ul id=”menu”>
<li><a href=”#”>Styles</a>
<ul>
<li><a href=”#”>Red/White</a></li>
<li><a href=”#”>Ros?©/Blush</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=”#”>Albari?±o</a></li>
<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>
</body>
</html>