Menu CSS

La plupart des sites et applications web modernes contiennent un menu. Construire un tel menu a longtemps impliqué l'utilisation intensive de javascript, mais cela est maintenant possible en utilisant uniquement les propriétés avancées des feuilles de style CSS2.

Ce type de solution nécessite évidemment un navigateur conforme au standard CSS2, ce qui exclut malheureusement le navigateur utilisé par 95% (environ) des utilisateurs : Internet Explorer. Cependant, il reste possible de construire une solution propre s'affichant correctement dans tous les navigateurs basés sur Gecko (Mozilla, Firebird, ...), et d'externaliser les adaptations spécifiques à Internet Explorer.

Menu basé sur une liste simple

Structure de base

Un menu est à la base une liste d'items à plusieurs niveaux, il est donc possible de la représenter simplement par des entrées HTML <ul> :

    <ul>
        <li>menu1 aa
            <ul>
                <li>menu1.1 cccc</li>
                <li>menu1.2 dddddd</li>
            </ul>
        </li>
        <li>menu2 bbbb
            <ul>
                <li>menu2.1 eeee</li>
                <li>menu2.2 ffffff</li>
            </ul>
        </li>
    </ul>
    

Affiché par défaut sous la forme :

Mise en forme

L'ajout de bordures permet de mieux représenter la structure du (futur) menu :

#menu1 ul { border:solid 1px green; }
#menu1 ul li { border:solid 1px red; }

Les sous-listes doivent être masquées par défaut (visibility:hidden;) et leur taille ne doit pas être prise en compte dans le calcul de la taille du menu (position:absolute) :

#menu2 ul { border:solid 1px green; }
#menu2 ul li { border:solid 1px red; }
#menu2 ul li ul { position:absolute;visibility:hidden; }

Pour un menu horizontal, le premier niveau de liste doit être sur une seule ligne, ce qui est obtenu par un display:table-cell sur les li de premier niveau, les li de deuxième niveaux doivent être en display:block:

#menu3 ul { border:solid 1px green; }
#menu3 ul li { display:table-cell;border:solid 1px red; }
#menu3 ul li ul { position:absolute;visibility:hidden; }
#menu3 ul li ul li { display:block; }

Dynamique

C'est le moment de passer au comportement dynamique, les styles de la forme element:hover permettent de changer de style au survol de la souris (non supporté par Internet Explorer). On change donc à visibility:visible; la sous liste contenu dans l'entrée li survolée.

#menu4 ul { border:solid 1px green; }
#menu4 ul li { display:table-cell;border:solid 1px red; }
#menu4 ul li ul { position:absolute;visibility:hidden; }
#menu4 ul li:hover ul { position:absolute;visibility:visible; }
#menu4 ul li ul li { display:block; }

Les sous-menus sont encore décalés par le padding par défaut des listes, ce qui est corrigé par un padding:0. La couleur de fond est transparente, et laisse donc apparaître le texte en dessous : background-color:lightgrey. Un changement de couleur des items au survol est aussi souhaitable : li:hover { background-color:grey}.

#menu5 ul { padding:0;border:solid 1px green; }
#menu5 ul li { background-color:lightgrey;display:table-cell;border:solid 1px red; }
#menu5 ul li:hover { background-color:grey}
#menu5 ul li ul { position:absolute;visibility:hidden; }
#menu5 ul li:hover ul { visibility:visible; }
#menu5 ul li ul li { display:block; }

Il reste à enlever les bordures inutiles :

#menu6 ul { padding:0; }
#menu6 ul li { background-color:lightgrey;display:table-cell; }
#menu6 ul li:hover { background-color:grey}
#menu6 ul li ul { border:solid 1px black;position:absolute;visibility:hidden; }
#menu6 ul li:hover ul { visibility:visible; }
#menu6 ul li ul li { display:block; }

Il s'agit évidemment d'une version de base à adapter à la charte graphique du site cible.

Support Internet Explorer

Principe

Le menu développé dans la première partie est conforme XHTML/CSS et fonctionne parfaitement dans les navigateurs respectant ces standards. Il ne fonctionne donc absolument pas dans Internet Explorer. Heureusement il existe des moyens de contourner les lacunes du navigateur principal du marché, sans (trop) dégrader le code décrit précédemment. La solution retenue consiste à utiliser une extension propriétaire Internet Explorer, les behaviors (comportements), afin d'isoler le code spécifique dans un fichier externe.

Création d'un comportement

Les comportements Internet Explorer permettent de rattacher des fonctions à des élements de la page via la feuille de style par une entrée behavior behavior:url('menu1.htc'); :

#menu7 ul { padding:0; }
#menu7 ul li { behavior:url('menu1.htc');background-color:lightgrey;display:table-cell; }
#menu7 ul li:hover { background-color:grey}
#menu7 ul li ul { border:solid 1px black;position:absolute;visibility:hidden; }
#menu7 ul li:hover ul { visibility:visible; }
#menu7 ul li ul li { display:block; }
Le fichier menu1.htc contient la définition des fonctions javascript permettant d'émuler le comportement standard en rattachant des fonctions aux évènements onmouseenter et onmouseleave.
<PUBLIC:COMPONENT>
   <PUBLIC:ATTACH EVENT="onmouseenter" ONEVENT="showSubMenu()" />
   <PUBLIC:ATTACH EVENT="onmouseleave"  ONEVENT="hideSubMenu()"  />

   <SCRIPT LANGUAGE="JScript">
    var menuBackground;
    var menuHighLight = 'rgb(128,128,128)';

    function showSubMenu() {
      // highlight current item
      menuBackground = style.backgroundColor;
      style.backgroundColor = menuHighLight;
    }
    
    function hideSubMenu() {
      // restore background
      style.backgroundColor = menuBackground;
    }

   </SCRIPT>
</PUBLIC:COMPONENT>
On remarque que la couleur au survol est écrite directe directement dans le fichier htc, il faut donc faire attention de le maintenir en cohérence avec le style CSS. Cette première version se limite à changer la couleur de fond des éléments li au survol de la souris :

Affichage des sous-menus

Il reste à récupérer le sous-menu correspondant à l'entrée, s'il existe, pour l'afficher :

<PUBLIC:COMPONENT>
   <PUBLIC:ATTACH EVENT="onmouseenter" ONEVENT="showSubMenu()" />
   <PUBLIC:ATTACH EVENT="onmouseleave"  ONEVENT="hideSubMenu()"  />

   <SCRIPT LANGUAGE="JScript">
    var menuBackground;
    var menuHighLight = 'rgb(128,128,128)';

    function showSubMenu() {
      // highlight current item
      menuBackground = style.backgroundColor;
      style.backgroundColor = menuHighLight;

      // get sub menu
      var submenu = getElementsByTagName('ul')[0];
      if (submenu) {
       
        // show sub menu
        submenu.style.visibility='visible';
      }
    }
    
    function hideSubMenu() {
      // restore background
      style.backgroundColor = menuBackground;

      // hide submenu
      var submenu = getElementsByTagName('ul')[0];
      if (submenu) {
        submenu.style.visibility='hidden';
      }
    }

   </SCRIPT>
</PUBLIC:COMPONENT>

Ajustement du style

Le style construit pour Gecko n'est pas rendu de la manière dans Internet Explorer, il faut donc adapter le style aux spécificités de ce navigateur. En particulier, le display:table-cell n'est pas supporté, il faut donc revenir à un display:inline sans changer la version interprétée par Gecko. L'une des solutions possibles est d'utiliser une autre extension propriétaire d'Internet Explorer : les expressions CSS, qui permettent de spécifier un style sous forme d'expression javascript : expression('inline').

#menu9 ul { padding:0; }
#menu9 ul li { behavior:url('menu2.htc');background-color:lightgrey;display:table-cell;display:expression('inline'); }
#menu9 ul li:hover { background-color:grey}
#menu9 ul li ul { border:solid 1px black;position:absolute;visibility:hidden; }
#menu9 ul li:hover ul { visibility:visible; }
#menu9 ul li ul li { display:block; }

Les listes sont décalées vers la droite de la marge par défaut :ul { margin:0;}. De plus, elles sont positionnées relativement au texte du li parent, il faut les caler à gauche sans impacter Gecko :left:expression(0);. Cela ne suffit pas, en effet, la position des sous-listes est calculée par rapport au premier élément parent repositionné, qui devrait être le li parent : ul li { position:expression('relative');}. Il reste à décaler les sous-listes vers le bas de la hauteur du li : top:expression(this.parentNode.offsetHeight+"px");

#menu10 ul { margin:0;padding:0; }
#menu10 ul li { behavior:url('menu2.htc');background-color:lightgrey;position:expression('relative');display:expression('inline'); }
#menu10 ul>li { display:table-cell; }
#menu10 ul li:hover { background-color:grey}
#menu10 ul li ul { top:expression(this.parentNode.offsetHeight+"px");left:expression(0);border:solid 1px black;position:absolute;visibility:hidden; }
#menu10 ul li:hover ul { visibility:visible; }
#menu10 ul li ul li { display:block; }

Résolution du problème des listes de choix

La dernière version du menu semble fonctionner correctement, jusqu'à ce qu'une liste de choix se trouve sous le menu :


Le menu passe sous la liste de choix ! Ce comportement est officiellement reconnu par Microsoft : il vient du fait que les objets select, pour une raison étrange, sont windowed. Cela signifie qu'ils ne tiennent pas compte du z-index appliqué aux autres éléments HTML. Une explication détaillée est disponible dans le document : Gestion des plans d'affichage sous Internet Explorer . Le contournement est particulièrement, comment dire, tordu : il s'appuie sur le fait que les objets iframe sont les seuls à tenir compte du z-index des objets windowed et non windowed. Il faut donc insérer un objet iframe derrière le menu et devant le select :
<PUBLIC:COMPONENT>
   <PUBLIC:ATTACH EVENT="onmouseenter" ONEVENT="showSubMenu()" />
   <PUBLIC:ATTACH EVENT="onmouseleave"  ONEVENT="hideSubMenu()"  />

   <SCRIPT LANGUAGE="JScript">
    var menuBackground;
    var menuHighLight = 'rgb(128,128,128)';

    function showSubMenu() {
      // highlight current item
      menuBackground = style.backgroundColor;
      style.backgroundColor = menuHighLight;

      // get sub menu
      var submenu = getElementsByTagName('ul')[0];
      if (submenu) {
       
        // workaround for windowed select object bug
        // that ignores zindex values
        var iframe = getElementsByTagName('iframe')[0];
        if (!iframe) {
          iframe = document.createElement('iframe');
          iframe.frameBorder=0;
          iframe.style.display='block';
          iframe.style.position='absolute';
          iframe.style.top=submenu.offsetTop;
          iframe.style.left=0;
          iframe.style.width=submenu.offsetWidth;
          iframe.style.height=submenu.offsetHeight;
          insertBefore(iframe, submenu);
        }
         
         // show sub menu
        submenu.style.visibility='visible';
        iframe.style.visibility='visible';
      }
    }
    
    function hideSubMenu() {
      // restore background
      style.backgroundColor = menuBackground;

      // hide submenu
      var submenu = getElementsByTagName('ul')[0];
      if (submenu) {
        submenu.style.visibility='hidden';
        getElementsByTagName('iframe')[0].style.visibility='hidden';
      }
    }

   </SCRIPT>
</PUBLIC:COMPONENT>

Conclusion

Il est maintenant possible de réaliser un menu entièrement en CSS. L'utilisation d'une simple liste permet de faciliter l'interprétation des pages par les outils d'accessibilité dédiés aux non ou mal voyants. Cependant, le contournement des déficiences du navigateur majeur du marché demande encore beaucoup d'efforts.

Télécharger le menu exemple : menu.zip.