Le <select> HTML natif est accessible par défaut, complètement gratuit, et pourtant régulièrement remplacé par un composant maison dès qu’un designer veut des options avec icônes, descriptions multi-lignes ou recherche instantanée. Le problème n’est pas de le remplacer : c’est de le remplacer sans reproduire son contrat d’accessibilité.
Ce tutoriel construit, étape par étape, un composant select personnalisé pour un thème WordPress, avec un vrai support clavier et lecteur d’écran, sans dépendance à une bibliothèque tierce lourde.
Étape 1 — Poser le balisage de base
La structure repose sur le pattern ARIA listbox. Un bouton déclencheur, suivi d’une liste d’options masquée par défaut :
<button type="button" class="select-bouton" aria-haspopup="listbox" aria-expanded="false">
Choisir un format
</button>
<ul class="select-liste" role="listbox" tabindex="-1" hidden>
<li role="option" data-valeur="pdf" id="opt-pdf">PDF</li>
<li role="option" data-valeur="epub" id="opt-epub">EPUB</li>
<li role="option" data-valeur="audio" id="opt-audio">Livre audio</li>
</ul>
Étape 2 — Ouvrir la liste et déplacer le focus

À l’activation du bouton (clic ou touche Entrée/Espace), la liste doit s’afficher et recevoir le focus. C’est la liste elle-même qui reste focus, avec un focus visuel géré sur l’option courante via aria-activedescendant plutôt qu’en déplaçant le focus réel sur chaque <li> :
bouton.addEventListener('click', function () {
var ouvert = liste.hidden === false;
liste.hidden = ouvert;
bouton.setAttribute('aria-expanded', String(!ouvert));
if (!ouvert) {
liste.focus();
definirOptionActive(premiereOption());
}
});
Étape 3 — Gérer les flèches et la sélection
Les flèches haut et bas déplacent l’option active, Entrée ou Espace valide la sélection, Échap referme la liste sans changer la valeur :
liste.addEventListener('keydown', function (event) {
switch (event.key) {
case 'ArrowDown':
event.preventDefault();
definirOptionActive(optionSuivante());
break;
case 'ArrowUp':
event.preventDefault();
definirOptionActive(optionPrecedente());
break;
case 'Enter':
case ' ':
event.preventDefault();
valider(optionActive());
break;
case 'Escape':
fermerSansValider();
break;
}
});
Étape 4 — Ajouter le typeahead
Un utilisateur de lecteur d’écran habitué au <select> natif s’attend à pouvoir taper la première lettre d’une option pour y sauter directement. Sans cette fonctionnalité, le composant paraît cassé même s’il respecte le reste du pattern ARIA :
- Capturer les frappes de caractères sur la liste ouverte
- Chercher la première option dont le libellé commence par la lettre tapée
- Réinitialiser la recherche après un court délai d’inactivité
Étape 5 — Synchroniser avec un champ caché pour le formulaire
Pour que le composant reste utilisable dans un formulaire WordPress classique, la valeur choisie doit être répercutée dans un champ <input type="hidden"> soumis normalement. Cela évite de réécrire toute la logique de traitement côté serveur.
Tester avant de livrer
La validation ne se limite pas à vérifier que la souris fonctionne. Le test minimal consiste à débrancher la souris et à parcourir tout le composant au clavier avec NVDA activé, en vérifiant que chaque option est annoncée avec son état sélectionné.
Un select personnalisé qui ne supporte pas le typeahead n’est pas un détail manquant : c’est un contrat rompu avec l’utilisateur qui connaît déjà le comportement standard.
En résumé
Reconstruire un select accessible demande de reproduire fidèlement trois choses : la navigation aux flèches avec aria-activedescendant, la fermeture cohérente au clavier, et le typeahead. Ce n’est pas plus compliqué qu’un menu déroulant classique, à condition de partir du pattern ARIA listbox plutôt que d’improviser une structure de <div> cliquables.