Implementing drop-down menus in pure CSS (no JavaScript)
by Anthony Williams
Introduction
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. I have implemented such a menu on a Sample web page, so you can try it in your browser.
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 following menu:
<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>
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; }
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.
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; }
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.
Design and Content Copyright © 2005-2024 Just Software Solutions Ltd. All rights reserved. | Privacy Policy