Implementing drop-down menus to aid website navigation is usually thought to require lots of JavaScript. This article shows how to do it using just CSS.
A client of mine wanted his website to have drop-down menus, so I had a look round at the best way of doing this. I imagined that it would require JavaScript, but it turns out that it is possible in pure CSS, at least for fully compliant browsers. This article attempts to explain how the CSS works, and builds up the menu step by step.
Why CSS?
Why CSS, and not JavaScript? JavaScript is often disabled by users, as a security measure, and the necessary code for drop-down menus can be quite involved. Also, a pure JavaScript menu is not available for browsers that don't support it, such as text-only browsers. CSS-based menus are always available, even with JavaScript disabled - browsers that don't handle it will just render a list. With this technique, adding a menu to a page is as easy as creating an unordered list of links, with nested lists for the sub-menus, and including the appropriate style-sheet.
Other CSS-based menus - what's new here?
Tarquin's tutorial on CSS menus shows how to do menus, where the main menu is stacked vertically, and the sub-menus open out to the side, and links to CrazyTB's CSS menu page, which shows a horizontal top-level menu, with drop-downs, but which doesn't work with IE, and imposes a fixed width on the menu entries. This article describes a technique for doing drop-down menus in CSS, with a horizontal top-level menu, and variable-width menu entries - in other words, I've managed to overcome many of the limitations of the implementations I've seen.
Menu structure
The menus are just represented by nested UL lists. Each LI is a menu entry, and nested lists result in sub-menus. The top level UL must have the class attribute of navmenu , and everything follows from there. The menu items are just normal A links, or SPAN s where they are not links. For this example, I'm going to use the menu in Listing 1.
<ul class="navmenu"> <li><a href="/tl1">Top Level Link</a></li> <li><a href="/tl2">SubMenus</a><ul> <li><a href="/tl2/item1">Item 1</a></li> <li><a href="/tl2/item2">Item 2</a></li> <li><a href="/tl2/item3">with submenus</a> <ul> <li><a href="/tl2/item3/one">One</a></li> <li><a href="/tl2/item3/two">Two</a></li> <li><a href="/tl2/item3/three">Three</a></li> </ul> </li> <li><a href="/tl2/item4">Item 4</a></li> </ul></li> <li><a href="/tl3">3rd entry</a><ul> <li><span>Submenu no link</span><ul> <li><a href="/tl3/item1/one">One</a></li> <li><a href="/tl3/item1/two">Two</a></li> <li><a href="/tl3/item1/three">Three</a></li> </ul></li> <li><a href="/tl3/item2">Item 2</a></li> </ul></li> <li><a href="/tl4">Fourth</a><ul> <li><a href="/tl4/item1">has items</a></li> <li><a href="/tl4/i2">but no submenus</a></li> </ul></li> <li><a href="/tl5">top level 5</a><ul> <li><a href="/tl5/i1">item 1</a></li> <li><a href="/tl5/i2">item 2</a></li> </ul></li> <li><a href="/tl6">entry 6</a><ul> <li><a href="/tl6/i1">foo</a></li> <li><a href="/tl6/i2">bar</a></li> </ul></li> <li><a href="/tl7">Final entry</a><ul> <li><a href="/tl7/i1">aaa</a></li> <li><a href="/tl7/i2">bbb</a></li> <li><a href="/tl7/i3">ccc</a></li> </ul></li> </ul> |
Listing 1 |
A menu bar should be horizontal
The first step is to take off all the normal list adornments, so we know what the indents are, and don't get bullet marks:
.navmenu, .navmenu ul, .navmenu li { padding: 0px; margin: 0px; } .navmenu li { list-style-type: none; }
We make the top level menu horizontal by floating the menu items, but if we do that then the rest of the page now displays underneath them, so we need to follow the menu with a clear style. In compliant browsers, we can do this using the .navmenu + * selector, but IE doesn't support this, so we need a tag with a class attribute of .endmenu following our menu (an empty DIV is good for that):
.navmenu li { float: left; } .navmenu + * { clear: left; } .endmenu { clear: left; }
Float and clear |
In normal usage, the float property of the CSS removes an element from the normal flow of the document, and "floats" it over to either the left or right edge. Such "floating" elements stack sideways, so if two elements both have a float: left style, then the second one will be to the right of the first. Subsequent content is flowed to the side of the floating elements. Here, we are using float to make the LI elements stack sideways, rather than their default of stacking vertically, in order to get a horizontal menu. The clear property is used with float , to ensure that following content appears below any floating elements. The value can be left or right , to indicate that this element (and any following ones) should be below all prior floating elements on the specified side, or both, to indicate that it should be below floating elements from either side. Here, clear is used to ensure that subsequent content comes below the menu bar. |
Sub-menus only display on demand
Next off is to hide the sub-menus, and show them when the mouse moves over the parent. This requires a browser that supports :hover for LI tags. For IE, we can then simulate this with DHTML Behaviours, as suggested by Tarquin:
.navmenu ul { display: none; } .navmenu li:hover > ul { display: block; } .navmenu ul.parent_hover { display: block; }
To add the DHTML Behaviours for IE, we can add the following to the HTML:
<!--[if gte IE 5]><![if lt IE 7]> <style type="text/css"> .navmenu li { behavior: url( ie_menus.htc ); } </style> <![endif]><![endif]-->
The DHTML behaviour file (ie_menus.htc) is then quite straightforward - we simply set the hover class on the current element, and parent_hover on all the child elements when the mouse moves over the appropriate element, and then remove these classes when the mouse moves off again:
<attach event="onmouseover" handler="mouseover" /> <attach event="onmouseout" handler="mouseout" /> <script type="text/javascript"> function mouseover() { element.className += ' hover'; for( var x = 0; x!=element.childNodes.length; ++x ) { if(element.childNodes[x].nodeType==1) { element.childNodes[x].className += ' parent_hover'; } } } function mouseout() { element.className = element.className.replace(/ ?hover$/,''); for( var x = 0; x!=element.childNodes.length; ++x ) { if(element.childNodes[x].nodeType==1) { element.childNodes[x].className = element.childNodes[x].className.replace( / ?parent_hover$/,''); } } } </script>
Sub-menu layout should be nice and clean
This works OK, but as the menus expand, the content of the rest of the page gets shunted down to make room. Ideally, we'd like the menus to show on top of the rest of the page. We can do this by giving the sub-menus a position style of absolute , but if we do just that then they're hard to see over the top of the text below, and the menus don't work quite right in IE. Therefore, we will add a border, and background. Of course, if we set a background colour, we ought to set the foreground colour too. Links have a different default colour to other text, so we need to set that separately. We therefore need to add the following styles:
.navmenu ul { position: absolute; } .navmenu li { border: 1px solid #3366cc; color: #000033; background-color: #6699FF; } .navmenu a { color: #000033; }
In Mozilla, the drop-down menus are also horizontal, whereas in IE, they're vertical. We can fix that by making only the top-level menu entries float , rather than all of them:
.navmenu > li { float: left; }
However, this doesn't work in IE - to get a nice horizontal top-level menu, we need to float the menu entries, and specify a fixed width for them, so we need to update our IE-specific block to do this:
<!--[if gte IE 5]><![if lt IE 7]> <style type="text/css"> .navmenu li { float: left; width: 8em; } </style> <![endif]><![endif]-->
Of course, you can vary the width as required.
Links should occupy the full box
I like the links to take up the full width of the box, so you don't have to click on the text. It's therefore nice to highlight the entire box when you hover the mouse, so you can see you're over a menu item that's actually a link.
In this case, because it's links we're referring to, IE is quite happy with :hover, so we can use the same styling for all browsers:
.navmenu a { display:block; width: 100%; text-decoration: none; } .navmenu a:hover { background-color: #f8f8fb; }
Sub-sub menus should pop out to the side
We now have a new problem - if one of the drop-down menus has a sub-menu, then we can't get to the following menu items, as the sub-menu comes down on top of them. We therefore need to adjust the positioning of the sub-menu; we'll move it almost to the right-hand edge of the parent menu item. It is important that we don't move it completely off, as then users would have to move the mouse cursor off the parent to go to the sub menu, and so the menu would close. We accomplish this with the left style. If we just use that, then the menus also start a line down, so we should use top to ensure they start level. Finally, we need to make the LI elements have relative positioning, since we made the sub-menus have absolute positioning above. This resets the base position for each sub-menu as relative to its parent menu item, rather than relative to the whole page.
.navmenu li { position: relative; } .navmenu ul ul { top: 0; left: 99%; }
The problem now is that the drop-down menus display underneath existing menus.
We could fix this with z-index , but IE doesn't handle that. Instead, and here's the fun bit, if we set padding-left to 1px , then the menu items are shown on top, but the top specified above doesn't work - it aligns the sub-menu with the top of the parent sub-menu.
Instead, we can use margin-top with a negative offset, to shift the block up. I've chosen -1.2em as the offset, since this is the default line-height, so the menu should pop out level with the parent entry.
.navmenu li { padding-left: 1px; } .navmenu ul ul { /* top: 0; --- remove this*/ margin-top: -1.2em; left: 99%; }
This left padding shifts the drop-down menus right a fraction. Combined with the border, this makes the top-level sub-menus appear 2 pixels to the right of their parent, which is a bit untidy. The fix for this issue is to add a negative margin to the sub menus, which has the effect of shifting them back left, to compensate:
.navmenu ul { margin-left: -2px; }
Menu items that have sub-menus, but are not themselves links, still don't work quite right, since the text does not form a block for CSS layout purposes, and the sub-menu therefore is displayed too high up. This is why we put the non-link menu items in SPAN tags - the fix for this is to make these SPAN tags into block elements:
.navmenu span { display: block; }
Exposed background
If the top-level menu doesn't cover the full width of the browser window, then the background for the BODY element will show through in the exposed parts. To deal with this, we can set the width of the outer UL element to 100% , and give it a background:
.navmenu { width: 100%; background-color: #6699FF; }
This works nicely in Opera and IE, but not in Firefox, which makes a change. If we also make it float to the left, then it works in all three browsers.
.navmenu { float: left; }
Spacing around text
Having the menu entries just display as minimal-sized blocks can mean that the text is quite close to the edges, and looks a bit crammed in. We can alleviate this by adding some padding to the LI elements:
.navmenu li { padding: 2px; }
Of course, this means that the previous padding-left entry should be removed, and the negative margin-left entry for the sub-menus needs adjusting. We also now need a margin-top entry for the first layer of sub-menus, to align the top of the sub-menu with the bottom of the parent item:
.navmenu li { /* padding-left: 1px; --- remove this */ } .navmenu ul { margin-left: -3px; /* was -2px */ margin-top: 2px; }
Another consequence of this padding is that the highlighted links now have an extra border around them, as only the link text area highlights, not the whole LI . We can fix that by changing the background colour for LI elements that we're hovering over as well. This has the side effect that the menu entries leading to the currently displayed sub-menu are also highlighted, which works as quite a nice visual aid. We'll leave the highlighting in place for hovered links, too, so that browsers that can't handle hovering on LI elements still show some highlighting. We have to do two versions here - one for IE, and one not, as we're relying on the DHTML behaviours for the hover detection in IE.
.navmenu li:hover { background-color: #f8f8fb; } .navmenu li.hover { background-color: #f8f8fb; }
Browser support
The key feature that this technique relies on is the ability to use the :hover modifier on arbitrary elements, and not just links. Older versions of browsers do not support this, but newer versions do. If this feature is not supported, just the top-level menu is shown. It is therefore important to ensure that the pages are not just accessible via the menu - I would recommend that each top-level menu entry is a link to a page with real links to the items on the appropriate sub-menu.
Internet Explorer doesn't support this usage of :hover, but it can be simulated with a small bit of JavaScript, as shown. If the user's security settings mean the JavaScript is not run, then IE will just display the top-level menu.
This technique is known to work in Opera 7.2 and 8.5, IE6 (with JavaScript), Mozilla Firefox 1.5, and Konqueror 3.4.3. It is known not to work in Opera 5, and Netscape 4.7. Of course, it doesn't work in text-only browsers such as Lynx, either - users of such browsers will see the menu just as a nested list.
Hiding things from old browsers
Old browsers such as Netscape Navigator 4.7 understand CSS, but get the rendering all wrong. Therefore, we need to mask our style-sheet from such browsers, so they just render the menu as a list. Of course, you could make a set of styles that worked with such browsers to make the menu render more nicely, if you wish. That's more effort than I'm willing to spend at the moment, so I'm just going to wrap the style-sheet in @media all{} , which will force such old browsers to completely ignore it.
There are numerous other techniques which can be used to adjust the style-sheet for specific older browsers, but they're beyond the scope of this article.
Conclusion
So, there you have it, drop-down menus in pure CSS, with a tiny bit of JavaScript for Internet Explorer. Supported in a wide range of browsers, with graceful degradation where it is not supported, this technique allows you to add menus to your website, without delving into JavaScript.
Final style-sheet
The final style sheet is shown on the next page, as Listing 2. The IE-specific styling, which needs to go directly into the HEAD part of the HTML, is shown in Listing 3. The IE-specific DHTML behaviour code from ie_menus.htc is in Listing 4.
@media all{ .navmenu, .navmenu ul, .navmenu li { padding: 0px; margin: 0px; } .navmenu > li { float: left; } .navmenu li { list-style-type: none; border: 1px solid #3366cc; color: #000000; background-color: #6699FF; padding: 2px; } .navmenu ul { display: none; position: absolute; margin-left: -3px; margin-top: 2px; } .navmenu li:hover > ul { display: block; } .navmenu ul.parent_hover { display: block; } .navmenu a { display: block; width: 100%; text-decoration: none; } .navmenu li:hover { background-color: #f8f8fb; } .navmenu li.hover, .navmenu a:hover { background-color: #f8f8fb; } .navmenu ul ul { margin-top: -1.2em; left: 99%; } .navmenu span { display: block; } .navmenu { float: left; width: 100%; background-color: #6699FF; } .endmenu { clear: left; } } |
Listing 2 |
<!--[if gte IE 5]><![if lt IE 7]> <style type="text/css"> .navmenu li { float: left; width: 8em; behavior: url( ie_menus.htc ); } </style> <![endif]><![endif]--> |
Listing 3 |
<attach event="onmouseover" handler="mouseover" /> <attach event="onmouseout" handler="mouseout" /> <script type="text/javascript"> function mouseover() { element.className += ' hover'; for( var x = 0; x!=element.childNodes.length; ++x ) { if(element.childNodes[x].nodeType==1) { element.childNodes[x].className += ' parent_hover'; } } } function mouseout() { element.className = element.className.replace(/ ?hover$/,''); for( var x = 0; x!=element.childNodes.length; ++x ) { if(element.childNodes[x].nodeType==1) { element.childNodes[x].className = element.childNodes[x].className.replace( / ?parent_hover$/,''); } } } |
Listing 4 |