Chapitre 41 : Stylisation avancée des menus déroulants avec :checked
Concepts clés : Manipulation de l'état des éléments select et options, implémentation du "Checkbox Hack" pour des menus personnalisés.
Le paradoxe du <select> et l'alternative :checked
1. Quoi
En CSS, l'élément <select> et ses enfants <option> sont des éléments dits "remplacés". Cela signifie que leur rendu est géré par le système d'exploitation (OS) et non par le moteur de rendu du navigateur. Par conséquent, il est quasiment impossible de styliser l'apparence d'une liste déroulante native (couleurs de fond des options, bordures du menu ouvert, animations de transition) de manière cohérente sur tous les navigateurs.
Pour contourner cette limitation, les développeurs utilisent une technique appelée le "Checkbox Hack". Elle consiste à utiliser un élément <input type="checkbox"> ou <input type="radio"> caché, couplé à la pseudo-classe :checked et au sélecteur de frère général (~), pour simuler l'ouverture et la fermeture d'un menu personnalisé.
2. Pourquoi
L'utilisation de :checked pour créer des menus déroulants est cruciale dans un projet professionnel pour plusieurs raisons :
- Contrôle Visuel Total : Vous pouvez appliquer des animations (transitions, transforms), des dégradés complexes et des ombres portées que le
<select>natif ignore. - Zéro JavaScript : Cette approche permet de créer des composants interactifs sans dépendre d'un script, ce qui améliore la performance et garantit le fonctionnement même si le JS est désactivé ou bloque.
- Expérience Utilisateur (UX) Cohérente : Le menu aura exactement le même aspect sur Chrome, Safari, Firefox et Edge, indépendamment du thème de l'OS de l'utilisateur.
3. Comment
A. Syntaxe de base
Le principe repose sur le lien entre un <label> et un <input>. Cliquer sur le label bascule l'état de l'input.
/* On cache l'input pour ne garder que l'aspect visuel du menu */
.menu-toggle {
display: none;
}
/* Le menu est caché par défaut */
.dropdown-content {
display: none;
}
/* QUAND l'input est coché, on affiche le menu qui suit dans le DOM */
.menu-toggle:checked ~ .dropdown-content {
display: block;
}
B. Cas concret : Menu de navigation stylisé
Voici une implémentation robuste. Nous utilisons des variables CSS pour faciliter la maintenance et des transitions pour l'aspect "premium".
<div class="dropdown-container">
<input type="checkbox" id="menu-trigger" class="menu-toggle" />
<label for="menu-trigger" class="menu-button">
Paramètres du compte
<span class="arrow">▼</span>
</label>
<ul class="dropdown-content">
<li><a href="#profile">Mon Profil</a></li>
<li><a href="#security">Sécurité</a></li>
<li><a href="#billing">Facturation</a></li>
<li class="divider"></li>
<li><a href="#logout" class="danger">Déconnexion</a></li>
</ul>
</div>
:root {
--primary-color: #4f46e5;
--text-color: #1f2937;
--bg-color: #ffffff;
--hover-color: #f3f4f6;
--danger-color: #ef4444;
--transition-speed: 0.3s;
}
.dropdown-container {
position: relative;
font-family: 'Inter', sans-serif;
width: 250px;
}
/* Masquage technique de l'input */
.menu-toggle {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
/* Style du bouton déclencheur */
.menu-button {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: var(--bg-color);
border: 1px solid #d1d5db;
border-radius: 8px;
cursor: pointer;
color: var(--text-color);
font-weight: 500;
transition: border-color var(--transition-speed);
user-select: none;
}
.menu-button:hover {
border-color: var(--primary-color);
}
/* Animation de la flèche lors de l'ouverture */
.menu-toggle:checked + .menu-button .arrow {
transform: rotate(180deg);
}
.arrow {
font-size: 0.8rem;
transition: transform var(--transition-speed);
}
/* Le menu déroulant */
.dropdown-content {
position: absolute;
top: calc(100% + 8px);
left: 0;
width: 100%;
background: var(--bg-color);
border: 1px solid #d1d5db;
border-radius: 8px;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
list-style: none;
padding: 8px 0;
margin: 0;
/* Animation d'entrée */
opacity: 0;
visibility: hidden;
transform: translateY(-10px);
transition: all var(--transition-speed) ease;
z-index: 100;
}
/* État ouvert : activation via :checked */
.menu-toggle:checked ~ .dropdown-content {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.dropdown-content li a {
display: block;
padding: 10px 16px;
text-decoration: none;
color: var(--text-color);
font-size: 0.9rem;
transition: background var(--transition-speed);
}
.dropdown-content li a:hover {
background: var(--hover-color);
}
.dropdown-content .divider {
height: 1px;
background: #e5e7eb;
margin: 8px 0;
}
.dropdown-content .danger {
color: var(--danger-color);
}
C. Limitations
Bien que puissant, le Checkbox Hack présente des inconvénients majeurs :
- Accessibilité (A11y) : Un
<input type="checkbox">n'est pas sémantiquement un bouton de menu. Les lecteurs d'écran annonceront "Case à cocher" au lieu de "Menu déroulant". Pour corriger cela, il faut ajouter des attributs ARIA (role="button",aria-expanded, etc.), mais cela nécessite souvent du JavaScript pour mettre à jour l'étataria-expanded. - Fermeture au clic extérieur : C'est le point faible principal. En CSS pur, il est impossible de détecter un clic en dehors du composant pour fermer le menu. L'utilisateur doit cliquer à nouveau sur le label pour fermer.
- Structure DOM rigide : Le sélecteur
~(frère général) ou+(frère adjacent) impose que l'input soit placé avant le menu dans le code HTML.
4. Zone de Danger
❌ Erreur commune : Tenter d'utiliser :checked sur un élément <select> ou <option>.
select:checked { ... } n'existe pas. La pseudo-classe :checked ne s'applique qu'aux éléments <input type="checkbox">, <input type="radio"> et <option> (dans certains contextes très limités et non stylisables).
✅ Bonne pratique : Utiliser un label avec l'attribut for lié à l'ID de l'input. Cela garantit que le clic sur le texte active l'input, même si celui-ci est masqué avec opacity: 0.
Flux logique du Checkbox Hack pour menus déroulants
Questions clés
1. Pourquoi ne peut-on pas simplement styliser les <option> d'un <select> ?
Découvrir la réponse
Parce que les éléments <option> sont rendus par l'OS (système d'exploitation) et non par le moteur CSS du navigateur. Ils sont considérés comme des éléments "externes" au DOM stylisable.
2. Quelle est la différence entre le sélecteur + et ~ dans le contexte du Checkbox Hack ?
Découvrir la réponse
Le sélecteur + (adjacent) cible l'élément immédiatement suivant. Le sélecteur ~ (général) cible n'importe quel élément suivant qui partage le même parent. Le ~ est plus flexible car il permet d'insérer d'autres éléments (comme le label) entre l'input et le menu.
3. Comment peut-on transformer ce menu en "menu à choix unique" (type Radio) ?
Découvrir la réponse
En remplaçant <input type="checkbox"> par <input type="radio">. Si plusieurs menus partagent le même attribut name, l'ouverture de l'un fermera automatiquement l'autre.
4. Le Checkbox Hack est-il recommandé pour des applications d'entreprise à haute accessibilité ?
Découvrir la réponse
Non. Pour des standards WCAG stricts, un bouton <button> géré par JavaScript avec des attributs aria-haspopup="true" et aria-expanded="false" est indispensable pour les utilisateurs de lecteurs d'écran.
5. Peut-on animer l'apparition du menu avec display: none ?
Découvrir la réponse
Non. La propriété display n'est pas animable. Pour obtenir une transition fluide, il faut utiliser une combinaison de opacity, visibility et transform (ou max-height).
Mise en pratique
Exercice 1 : Reproduction guidée
Objectif : Créer un menu "Toggle" simple.
- Créez un input checkbox caché.
- Créez un label "Afficher les détails".
- Créez un bloc
divcontenant du texte, caché par défaut. - Faites en sorte que le bloc s'affiche avec un fondu (
opacity) quand la case est cochée.
Découvrir la solution commentée
<input type="checkbox" id="toggle-details" class="hidden-check" />
<label for="toggle-details" class="btn-toggle">Afficher les détails</label>
<div class="details-box">
Voici les informations secrètes !
</div>
/* CSS */
.hidden-check {
display: none;
}
.btn-toggle {
padding: 10px;
background: #eee;
cursor: pointer;
border: 1px solid #ccc;
}
.details-box {
margin-top: 10px;
padding: 10px;
background: #f9f9f9;
opacity: 0;
transition: opacity 0.5s ease;
pointer-events: none; /* Empêche l'interaction quand invisible */
}
/* Activation */
.hidden-check:checked ~ .details-box {
opacity: 1;
pointer-events: auto;
}
Exercice 2 : Adaptation
Objectif : Transformer un système d'onglets "cassé" (tous les contenus visibles en même temps) en un système fonctionnel grâce à :checked.
HTML et CSS de départ :
<div class="tabs-container">
<input type="radio" name="tab-group" id="tab1" checked />
<label for="tab1">Onglet 1</label>
<input type="radio" name="tab-group" id="tab2" />
<label for="tab2">Onglet 2</label>
<input type="radio" name="tab-group" id="tab3" />
<label for="tab3">Onglet 3</label>
<div class="content content1">Contenu de l'onglet 1</div>
<div class="content content2">Contenu de l'onglet 2</div>
<div class="content content3">Contenu de l'onglet 3</div>
</div>
/* CSS de départ — à compléter */
.tabs-container input {
display: none;
}
.tabs-container label {
padding: 10px;
cursor: pointer;
display: inline-block;
border: 1px solid #ccc;
}
.content {
padding: 20px;
border: 1px solid #ccc;
margin-top: 10px;
/* PROBLÈME : tous les onglets sont visibles en même temps */
}
Ce que vous devez corriger :
- Masquer tous les blocs
.contentpar défaut. - Afficher uniquement le
.contentcorrespondant au radio coché (utilisez le sélecteur~). - Mettre en évidence visuellement l'onglet actif (label du radio coché).
Découvrir la solution commentée
<div class="tabs-container">
<input type="radio" name="tab-group" id="tab1" checked />
<label for="tab1">Onglet 1</label>
<input type="radio" name="tab-group" id="tab2" />
<label for="tab2">Onglet 2</label>
<input type="radio" name="tab-group" id="tab3" />
<label for="tab3">Onglet 3</label>
<div class="content content1">Contenu de l'onglet 1</div>
<div class="content content2">Contenu de l'onglet 2</div>
<div class="content content3">Contenu de l'onglet 3</div>
</div>
.tabs-container input {
display: none;
}
.tabs-container label {
padding: 10px;
cursor: pointer;
display: inline-block;
border: 1px solid #ccc;
}
.content {
display: none;
padding: 20px;
border: 1px solid #ccc;
margin-top: 10px;
}
/* Logique de sélection : on cible le contenu spécifique
en fonction de l'input coché */
#tab1:checked ~ .content1,
#tab2:checked ~ .content2,
#tab3:checked ~ .content3 {
display: block;
}
/* Style pour le label actif */
#tab1:checked + label,
#tab2:checked + label,
#tab3:checked + label {
background: #4f46e5;
color: white;
}