:hovercraft

be gone evil scriplets!

by Peter Nederlof

More and more people realize that the proper use of stylesheets and semantically correct markup can increase both the usability and accessibility of websites. But how do we go about creating more complex components - the ones we used to create using heaps of <divs> and script - in an elegant and responsible way?

Often a short script provides the answer for completing an otherwise perfect solution. For instance, take the Suckerfish Dropdowns article, which advocates the use of a short script to show and hide the submenus. Other solutions include mouseover and mouseout attributes to highlight table rows, form elements and other page elements. This is already a step in the right direction, but doesn't CSS itself provide the answer to all these issues with the :hover pseudo-class?

:hover pseudo-class

The :hover pseudo-class allows you to define styles for elements the user points at with the mouse. Mostly this is used for rollover effects on links. Officially though, you should be able to use :hover on any element, thus helping the user with visual clues on your site. To a lesser extent this should also be the case for :active and :focus. These three classes combined should cover usability and accessibility options for all input devices you can use to navigate a site.

Should is stressed for an obvious reason; "officially" is one thing, in reality however the vast majority of users is surfing the web with Internet Explorer, the one single browser that does not support any of these features, except for :hover and :active on links. This makes it all but impossible to use these features without providing an alternative like the scripts mentioned before. This might not be that big an issue when highlighting table rows with :hover this way:

tr:hover { background-color:#f0f0f0; }

But it should also be possible to show and hide submenus of a menu system with a :hover. Take a look at the example below for instance:

<ul> <li><a href="">item</a></li> <li> <a href="">item</a> <ul> <li><a href="">item</a></li> <li><a href="">item</a></li> <li><a href="">item</a></li> </ul> </li> <li><a href="">item</a></li> </ul>

Without any CSS code this would just show a list, with another list nested inside the second item. But with the CSS code below the list would be transformed into a very basic menu system (disregarding IE's lack of support for the moment), where nested lists act as submenus:

ul ul { display:none; } li:hover > ul { display:block; }

The first rule will hide all submenus, since these don't need to be visible right away. The second rule will display a submenu, but because of the > child selector it will display only the one that is nested directly in the list item the user is pointing at. Menus that are nested within it will remain hidden until the user points at their parent list item with the mouse. In addition to this, when looking at the different levels of such a menu system all preceding submenus will remain visible. The :hover remains active for the parent list items because the menus are nested.

This way the use of javascript would be unnecessary altogether, even with script disabled the menu would still be fully functional. But Internet Explorer supports neither the > child selector, nor this kind of use of :hover, making the entire concept unusable.

This means that a solution making use of javascript is still required, but in stead of coding a separate solution for every component you create, a generic solution could look at the CSS itself and automatically apply a fix. That is exactly what we are going to do. CSS does after all contain all the required information: the affected elements and the rollover effect. The fact that CSS is a standard makes the advantages even more obvious.

Rebuilding the :hover

To recreate the hover in Internet Explorer, only a few things need to be done:

A frequently used method to simulate rollover effects is to alter the class name of an element based on specific events. The script is going to automate this by copying rules that contain :hover, changing them to class names. Changing for instance from:

li:hover ul {}

to:

li.onHover ul {}

These rules are then added to the stylesheet the original rules came from. After this the script will look up all the affected elements and apply mouseover and mouseout events to them to change class names correctly. Because this fix only needs to work in IE - since all other browsers already support what we're trying to fix - it's best to attach it to documents using an IE only behavior.

Behaviors are linked to elements using the stylesheet, and enable you to give them specific features. For instance, there's the download behavior which makes it easier to dynamically download files from the server. Another one is the userData behavior, which acts much like a big cookie and can contain a limited amount of persistent data.

Aside from a large amount of predefined behaviors, you can create your own behaviors too. These are called HTML Components (htc) and can be saved (in plain text) as .htc file. Just like the regular behaviors, these too can be linked to elements using the stylesheet. Because the hover script only has to be executed once, the best element to apply it to would be the body itself:

body { behavior:url("csshover.htc"); }

The final behavior: csshover.htc

It should be noted that to a certain extent this is a form of abuse of behaviors, since they normally add features to elements, and this one does not (not directly at least). Considering the goal of the script though - to enable something only in IE - it could also be regarded as the only good way to link the script.

Another thing is that this solution won't work if script is disabled. In that case elements that are made invisible with CSS and rely on rules that contain :hover to be made visible will remain hidden. So you should always provide an alternative that is not dependent on scripting or CSS. Without CSS (and with scripting) nothing will happen, since there will be no rules to fix, and hence no :hover to respond to.

Specificity

In the case of a menu system with only a single level of subnavigation you could safely use the descendant selector (space) to show and hide submenus as described above. Menus with multiple levels of subnavigation require a different approach. A rollover event on a list-item would open all levels instead of only the next level, but we can't use the > child selector, because IE simply does not support it. One possible solution would be to apply a unique class name to every sub level, for instance:

ul.level1 li:hover ul.level2, ul.level2 li:hover ul.level3 { ... }

While this will work, it is probably not the best approach. The CSS code is relatively long, and what's worse: you'll have to consistently add class names for non-layout purposes to every list in you menu. Fortunately, there is a much better solution.

Every CSS rule has a certain specificity, which defines whether or not it overrules others. The higher the specificity of a rule, the more important it becomes. This specificity is calculated simply by counting the occurrences of ids, (pseudo-)classes and attribute selectors, and node names. Ids can be considered to count as 100, classes and other attributes as 10, and every node name as 1. In the example below:

ul ul { display:none; } li:hover ul { display:block; }

... the first rule would count as "2", because of the two node names. The second rule would count as 12, because of the two node names and the :hover pseudo-class. This is in fact the very reason why the second rule works: because its specificity is higher than the first rule, a rollover on list-items will display any nested lists within. This may not seem like such a big deal, but we can actually use this concept to hide submenus that don't need to be visible just yet, and only show them when needed later on.

If we need a third level of navigation to be hidden during a rollover somewhere in the first level, all we need to do is make a rule that hides it and is worth 13 or more. To show that third level, there needs to be a rule for a rollover in the second level, again with a greater specificity than the previous rule that hid it. Like the solution that relied on classes, this one also can't be made of one single rule, but nonetheless, the CSS code for 3 levels is surprisingly short:

ul ul, li:hover ul ul { /* specificity: 2 and 13 */ display:none; } li:hover ul, li:hover li:hover ul { /* specificity: 12 and 23 */ display:block; }

The specificity of the different selectors has been added for clarity. As described, the left part of the first and second rule hide and show any existing submenus. The right part of the first rule now hides third level navigation because of its greater specificity. The right part of the second rule overrules it when a rollover in the second level occurrs, and will cause the third and final level to become visible.

Wrapping it up

To see all this in action, take a look at the example menu. It combines the behavior that enables :hover, and relies on specificity to show and hide submenus.

By combining existing technologies it is possible to create fixes that make standard compliant code work without having to think about it. Fixing the :hover pseudo-class is just one example of this: it is by no means the first attempt, merely a different approach to "fixing" things. More solutions that address annoyances we live with today - and will have to live with for quite some time to come - are very likely to keep surfacing every now and then. All of these solutions show us that with a bit of creativity some technologies of tomorrow can already be used today.

Back