Summary
A strictly structure, presentation and function seperated n depth DHTML menu. The power of HTML coupled with CSS and JavaScript is illustrated.
Requirements
To view the demo and run the source: Internet Explorer 6 (no ASP or any other server side support required.)
To follow this article: Basic HTML and medium to expert CSS and JavaScript skills.
Updates
I prefer notifying people of updates at the top of the article
- (2002/10/25) Mozilla 1.0 support:
Many thanks to Nikhil Dabas for providing the W3C DOM compliant JavaScript code which means the menu now works in Mozilla and Internet Explorer. Also amazingly the CSS itself required just one tweak to get the menu displaying in Mozilla almost perfectly. The menu can be a touch finicky in Mozilla (sometimes the sub-menu does not close, if you can figure it out, then a browny point for you. I suspect it has to do with event bubbling) but it is usable enough to be useful. For those interested the CSS tweak involved simply telling the menu-item LI
elements to have a width of 100% (actually 98% because 100% overwrites the border of the containing UL
element.) Also I moved the sub-menus slightly up and to the right, to not overwrite the containing menu (good idea Nihkil.)
Introduction
There is an endless list of DHTML menu samples on the world wide web. From intricate cross browser samples to extravagant DirectX enabled wonders. This article demonstrates a DHTML menu that is none of these, what it does demonstrate is how to create a DHTML menu which has strict seperation of structure, presentation and function.
Seperation of these three areas is important for the following reasons:
- Forward and backward compatibility
By not tightly coupling the three areas together future browsers are guaranteed at the very least to render the content and structure well enough to be usable. Also in older browsers if the browser does not support advanced CSS or JavaScript, it at least supports the content and structure, so once again ensuring the menu is usable.
- Accessibility
By not mixing the content and structure up with presentation and function accesibility devices (like text-readers for the blind) can easily access and understand the content. Search engines will also index well structured content better than mixed content.
- Maintenance
Adding or removing menu items from this kind of setup is dead simple. Other setups may require extra CSS classes, further JavaScript and complex structural changes (not too mention understanding it all.)
- Skill use
Much like ASP.NET encourages developers to focus on what they are good at, so this setup does as well. UI specialists can modify the CSS file without interfering with a developer working on the JavaScript file or a content specialist working on the HTML file.
- Information Access
The web is all about providing information to all. Whatever device you are using, a web page should be at least accesible enough to read the content and navigate the website. The seperation shown in this article is perfect for multiple devices in that it will degrade gracefully depending on what device is accessing the page. e.g. A handheld browser will ignore the JavaScript and CSS and just render the menu as a list, making it usable and accesable. A smidgen of device detection will also improve things as some devices attempt to render beyond their capibilities.
The Menu
The actual menu is very simple. It consists of three parts: The HTML (structure and content), the CSS file (presentation or style) and the JavaScript file (function or workings.)
The menu can display as much depth (sub-menus) as you need, though of course screen real estate can become a practical concern if you go too deep. Also each menu list can display as many menu items as you need, with as many sub-menus in one list as is needed.
The function of the menu (the onmouseover
and onmouseout
events) are only applied onload
of the BODY
. This is done to ensure that all structure is loaded before the JavaScript attempts to do it's thing.
You do not need to define a CSS style for each sub-menu. The position of the sub-menus is automatic (and explained in the CSS section below.)
The HTML
The complete HTML is in the source file.
...
<body onload="initialiseMenu();">
<h1>DMenu: Sample Simple</h1>
<ul id="mainmenu">
<li><a href="about/">About Us</a></li>
<li><a href="about/">Articles</a>
<ul>
<li><a href="articles/">C#</a>
<ul>
<li><a href="articles/">C# Reference</a></li>
<li><a href="articles/">Tutorials</a></li>
<li><a href="articles/">Samples</a></li>
<li><a href="articles/">FAQ</a></li>
</ul>
</li>...
...</ul>
</li>
<li><a href="about/">Tools</a>
<ul>
<li><a href="articles/">Assemblers</a></li>
<li><a href="articles/">Disassemblers</a></li>
<li><a href="articles/">CP Utilities</a>
</ul>
</li>
<li><a href="contactus/">Contact Us</a></li>
</ul>
...
As you can see it looks just like a normal UL
(un-ordered list) arrangement. Inside each LI
element is an A
element which controls the hyperlinking.
According to the W3C the proper way to nest lists is as above, e.g. the sub-UL is within the LI element and not outside of it. This is critical also for the JavaScript to work properly (i.e. if you don't nest the lists properly then the JavaScript will not work.)
The only setup note you need to know about really is the onload="initialiseMenu();"
declaration in the BODY
element. If you do know how to attach the initialiseMenu
method to the onload
event from within the JavaScript file then please email me, thanks.
The JavaScript
The HTML is dead simple and nothing new. But the JavaScript is quite fun and interesting. I am going to step through it line by line:
function showSubMenu(){
var objThis = this;
for(var i = 0; i < objThis.childNodes.length; i++)
{
if(objThis.childNodes.item(i).nodeName == "UL")
{
objThis.childNodes.item(i).style.display = "block";
break;
}
}
}
Very simply this is the method that is called on mouse over of an LI
element. As you will see below the onmouseover
event is assigned but no parameter is passed indicating what element to show or what element fired the event. Luckily when you do this assigning and the event fires the browser keeps a reference of what element fired the event and that is accessed via the this
object. Very handy indeed.
At this point you have this
and all you need to do is loop through the child elements until the UL
element is found. Once found the style.display
attribute value is changed to block
.
On an aside the childNodes
method returns all child elements of an element, including literals, so be careful when using an index to reference a child element (thanks for that Nikhil!.)
function hideSubMenu()
{
var objThis = this;
for(var i = 0; i < objThis.childNodes.length; i++)
{
if(objThis.childNodes.item(i).nodeName == "UL")
{
objThis.childNodes.item(i).style.display = "none";
break;
}
}
}
Virtually identical to the showSubMenu
method, except it changes the style.display
attribute to none
which effectively hides the element.
function initialiseMenu()
{
var objLICollection = document.body.getElementsByTagName("LI");
for(var i = 0; i < objLICollection.length; i++)
{
var objLI = objLICollection[i];
for(var j = 0; j < objLI.childNodes.length; j++)
{
if(objLI.childNodes.item(j).nodeName == "UL")
{
objLI.onmouseover=showSubMenu;
objLI.onmouseout=hideSubMenu;
for(var j = 0; j < objLI.childNodes.length; j++)
{
if(objLI.childNodes.item(j).nodeName == "A")
{
objLI.childNodes.item(j).className = "hassubmenu";
}
}
}
}
}
}
initialiseMenu
is the fun method. It is called onload
of the documents body
.
First it gets a complete collection of all LI
elements in the documents DOM tree. getElementsByTagName
is a very handy method for returning a collection of nodes of any element type. It can also be used off of almost any element (i.e. not just body
) allowing you to contain the scope of the get. For instance it would probably be better in this article to use document.body.all.mainmenu.getElementsByTagName("LI")
so that no other non-menu LI
elements are returned.
Next the code loops through the collection by getting the length of the collection objLICollection.length
.
For each node in the collection it then loops through it's child elements looking for a UL
element.
If one is found it assigns the showSubMenu
method to the onmouseover
event and the hideSubMenu
method to the onmouseout
event of the current LI
element. Ensure when you do this assigning that you do not end off the assignment with ()
as is usually done for methods. If you do this then the method is run on the actual assignment (another hard lesson learnt.)
The last thing to do is to once again loop through the child elements and assign the hasssubmenu
CSS class to the child A
element. This simply displays the arrow image to indicate to the user that the menu item has a sub-menu.
The CSS
The CSS is short but sometimes tricky. I will step through each relevant class:
...
ul, li
{
font-size: small;
margin-top: 0px;
margin-right: 0px;
margin-bottom: 0px;
display: block;
}
...
Just give all UL
and LI
elements no margins, a font-size of small and a display type of block.
...
ul
{
width: 130px;
border: solid 1px #333333;
border-top: solid 5px #333333;
border-right: solid 2px #333333;
}
...
Give all UL
elements a width of 130px. Then assign a solid, 1px dark grey border to the UL
, but override the right and top borders to have a width of 5px. You could define each border attribute individually, but this is just a quicker way.
...
ul li ul
{
display: none;
position: absolute;
}
...
ul li ul
is a CSS parent/child selector. Basically it is saying "apply the following styles to any UL which is a child of an LI which is a child of a UL." This way you prevent the top level menu UL
being given the style (which is important otherwise the top level menu UL would be hidden from the start.)
display: none;
effectively says not to display the element in any way and that no space is taken up by it. visibility: hidden
for instance does not do the same thing. It makes the elment invisible, but the element still takes up space.
position: absolute;
takes the element out of the normal flow of the document and "floats" it above other relative or statically position elements. This way when the element is made visible, it will not "bump" other elements downwards in the flow of the document. Also it is important to note that this absolute
value is still relevant to the elements container. This is what allows the sub-menus to automatically position themselves.
...
li a
{
padding: 2px;
text-decoration: none;
color: #000000;
background-color: #ffffee;
width: 100%;
display: block;
border-bottom: dashed 1px #333333;
text-indent: 2px;
font-size: small;
}
...
Most of this is dedicated to making the menu items look nice. e.g. the dashed bottom border, the background colour etc. Once again a parent/child selector is used: li a
. So the style will only be applied to A
elements which are children of an LI
element.
The only important part is the width: 100%;
attribute which tells the A
element to fill up, width ways, all available space of the parent element. This ensures the mouseover effect happens on the whole menu-item and not just the letters of the A
element.
Also the text-indent
attribute is not often used but is quite useful when you want to ensure just text is padded away from the borders (padding
can also be used, but does not always work in some cases.)
...
li a:hover
{
background-color: #ffcc00;
font-weight: bold;
border-bottom: solid 1px #333333;
}
...
This just defines the style of the hover
state (when the mouse is over the element.) One day (XHTML 2.0) this kind of functionality (and putting HREF
on any element!) is going to be supported for all elements, which will be just great.
...
li
{
float: left;
width: 98%;
}
...
This one seems simple, but is critical. float
can be your best friend and your worst enemy. What it does is allow elements to literally float alongside each other, without a line break. You can float left and right. The problem comes when you want other elements to stop floating alongside a certain element. In that case you need to use the clear: both
attribute and value, which tells the element not to allow any other elements to float alongside it on either side.
width: 98%;
tells the LI
element to fill up, width ways, all available space. 100% though unfortunatley overwrites the containing UL
's border style in Mozilla. 98% does the trick though and works in Internet Explorer quite fine.
We are almost at the end.
...
a.hassubmenu
{
background-image: url(../img/lay_dmnuhassub.gif);
background-repeat: no-repeat;
background-position: 120px 7px;
}
...
Remember the className
assignment in the JavaScript? Well this is the class that is assigned to LI
elements which have sub-menus.
All it really does is display the lay_dmnuhassub.gif graphic in the background of the element. background-repeat: no-repeat;
is quite useful in that you can control on which plane the background repeats (hence the name.) You can specify repeat-x
, repeat-y
, repeat
and no-repeat
as valid values.
background-position: 120px 7px;
is also useful and lets you positon the start of a background graphic. It is an x, y co-ord.
Possible Improvements & Bugs
- Pre-load the hassubmenu arrow graphic to prevent it from re-loading each time a sub-menu is displayed. If anyone has some quick code to do this, please feel free to share.
- Try and fix the hiding of sub-menus bug in Mozilla. I assume it has something to do with event bubbling.
- Seperate the important CSS style attributes and values from the purely presentational attributes and values. This way a "base" DHTML menu could be provided and then styling (colours, borders, backgrounds etc.) applied without fear of breaking the menu.
- Figure out how to attach the onload event handler to the event from within the JS file but without having to move the script block out of the head area (While perfectly valid, I am picky about where my script blocks go. Mainly for maintenance and understanding purposes.)
- Instantly upgrade every single users browser to either Inernet Explorer 6 or Mozilla 1.0. ;)
Conclusion
And that folks is a wrap. All in all quite simple and a very elegant solution to producing DHTML menus.
For now the DHTML menu is perfect for a forwards thinking website content with not having a DHTML menu on older browsers or on company intranets where you can control what browser users use. In the future with the above tweaks this kind of web code is going to become more and more popular.
Overall though the concept I am driving at is seperation of structure/content, presentation/style and function. This is critical to grasp for the web to move forward and in creating efficient development and maintenance teams. Without it we remain with our hard to maintain and non-forwards-compatible websites.