Modales accessibles : implémentation ARIA et gestion du clavier
En Bref : L'essentiel à retenir
- Une modale accessible nécessite role="dialog", aria-modal="true", aria-labelledby et une gestion du focus rigoureuse.
- Le focus trap empêche les utilisateurs clavier de sortir de la modale tant qu'elle est ouverte.
- À l'ouverture, le focus doit se déplacer dans la modale ; à la fermeture, il doit revenir à l'élément déclencheur.
- La touche Échap doit fermer la modale, et un bouton de fermeture visible doit toujours être présent.
Les fenêtres modales sont parmi les composants les plus complexes à rendre accessibles. Entre la gestion du focus, l'isolation du contenu arrière-plan et les attributs ARIA, les points d'attention sont nombreux. Ce guide technique vous accompagne dans l'implémentation d'une modale véritablement accessible.
Anatomie d'une modale accessible
Une modale accessible se compose de plusieurs éléments essentiels :
- L'overlay (backdrop) : Fond semi-transparent isolant la modale
- Le conteneur : La fenêtre modale elle-même
- L'en-tête : Titre et bouton de fermeture
- Le contenu : Corps de la modale
- Les actions : Boutons de validation/annulation
Structure HTML complète
<!-- Overlay -->
<div class="modal-overlay" data-close-modal></div>
<!-- Modale -->
<div
class="modal"
role="dialog"
aria-modal="true"
aria-labelledby="modal-titre"
aria-describedby="modal-description"
>
<!-- En-tête -->
<header class="modal-header">
<h2 id="modal-titre">Confirmation de suppression</h2>
<button
type="button"
class="modal-close"
aria-label="Fermer la fenêtre"
data-close-modal
>
<svg aria-hidden="true"><!-- icone X --></svg>
</button>
</header>
<!-- Contenu -->
<div class="modal-body">
<p id="modal-description">
Êtes-vous sûr de vouloir supprimer cet élément ?
Cette action est irréversible.
</p>
</div>
<!-- Actions -->
<footer class="modal-footer">
<button type="button" data-close-modal>Annuler</button>
<button type="button" class="btn-danger">Supprimer</button>
</footer>
</div>
Les attributs ARIA essentiels
role="dialog"
Identifie l'élément comme un dialogue pour les technologies d'assistance :
<div role="dialog">
Les lecteurs d'écran annoncent "dialogue" ou "fenêtre de dialogue" à l'entrée.
aria-modal="true"
Indique que le dialogue est modal et que le contenu sous-jacent est inerte :
<div role="dialog" aria-modal="true">
[!NOTE]
aria-modal="true"prévient les technologies d'assistance que seul le contenu de la modale est accessible. Cependant, vous devez également rendre le contenu arrière-plan réellement inerte via JavaScript ou l'attributinert.
aria-labelledby
Référence l'élément qui titre la modale :
<div role="dialog" aria-labelledby="modal-titre">
<h2 id="modal-titre">Titre de la modale</h2>
</div>
Le lecteur d'écran annoncera : "Titre de la modale, dialogue".
aria-describedby (optionnel)
Référence une description supplémentaire du contenu :
<div
role="dialog"
aria-labelledby="modal-titre"
aria-describedby="modal-desc"
>
<h2 id="modal-titre">Confirmation</h2>
<p id="modal-desc">Cette action supprimera définitivement vos données.</p>
</div>
Gestion du focus : le coeur de l'accessibilité
Focus initial à l'ouverture
Quand la modale s'ouvre, le focus doit se déplacer à l'intérieur. Plusieurs stratégies sont possibles :
Option 1 : Focus sur le premier élément interactif
function ouvrirModale(modal) {
modal.hidden = false;
const premierFocusable = modal.querySelector(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
premierFocusable?.focus();
}
Option 2 : Focus sur le conteneur modal
Utile si le contenu commence par du texte à lire :
<div role="dialog" tabindex="-1">
function ouvrirModale(modal) {
modal.hidden = false;
modal.focus();
}
Option 3 : Focus sur un élément spécifique
Pour les modales avec formulaire, focuser le premier champ :
function ouvrirModale(modal) {
modal.hidden = false;
const premierChamp = modal.querySelector('input, select, textarea');
premierChamp?.focus();
}
Le focus trap (piège de focus)
Le focus doit rester à l'intérieur de la modale tant qu'elle est ouverte :
class ModalAccessible {
constructor(modal) {
this.modal = modal;
this.focusableElements = null;
this.premierElement = null;
this.dernierElement = null;
}
updateFocusableElements() {
const selecteurs = [
'a[href]',
'button:not([disabled])',
'input:not([disabled])',
'select:not([disabled])',
'textarea:not([disabled])',
'[tabindex]:not([tabindex="-1"])'
].join(', ');
this.focusableElements = this.modal.querySelectorAll(selecteurs);
this.premierElement = this.focusableElements[0];
this.dernierElement = this.focusableElements[
this.focusableElements.length - 1
];
}
trapFocus(event) {
if (event.key !== 'Tab') return;
// Shift + Tab sur le premier élément -> aller au dernier
if (event.shiftKey && document.activeElement === this.premierElement) {
event.preventDefault();
this.dernierElement.focus();
}
// Tab sur le dernier élément -> aller au premier
else if (!event.shiftKey && document.activeElement === this.dernierElement) {
event.preventDefault();
this.premierElement.focus();
}
}
activer() {
this.updateFocusableElements();
this.modal.addEventListener('keydown', (e) => this.trapFocus(e));
}
}
Retour du focus à la fermeture
Mémorisez l'élément déclencheur et restaurez le focus :
class ModalManager {
constructor() {
this.elementDeclencheur = null;
this.modaleActive = null;
}
ouvrir(modal, déclencheur) {
this.elementDeclencheur = déclencheur;
this.modaleActive = modal;
modal.hidden = false;
// ... focus initial
}
fermer() {
this.modaleActive.hidden = true;
// Retour du focus au déclencheur
this.elementDeclencheur?.focus();
this.modaleActive = null;
this.elementDeclencheur = null;
}
}
[!TIP] Si l'élément déclencheur n'existe plus après la fermeture (suppression réussie par exemple), focusez un élément logique proche ou le début du contenu principal.
Gestion du clavier
Fermeture avec la touche Échap
La touche Échap doit toujours fermer la modale :
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && this.modaleActive) {
this.fermer();
}
});
Navigation Tab standard
La touche Tab doit naviguer entre les éléments focusables dans l'ordre du DOM. Le focus trap s'occupe de boucler.
Rendre le contenu arrière-plan inerte
L'attribut inert (moderne)
L'attribut HTML inert rend un élément et ses descendants non interactifs :
function ouvrirModale(modal) {
// Rendre tout le reste de la page inerte
document.querySelectorAll('body > *:not(.modal-container)').forEach(el => {
el.inert = true;
});
modal.hidden = false;
}
function fermerModale(modal) {
// Restaurer l'interactivité
document.querySelectorAll('[inert]').forEach(el => {
el.inert = false;
});
modal.hidden = true;
}
Fallback avec aria-hidden
Pour les navigateurs sans support de inert :
function ouvrirModale(modal) {
document.querySelectorAll('body > *:not(.modal-container)').forEach(el => {
el.setAttribute('aria-hidden', 'true');
});
}
[!NOTE]
aria-hiddencache le contenu des lecteurs d'écran mais ne bloque pas l'interaction clavier. Combinez avectabindex="-1"sur les éléments interactifs ou utilisez l'attributinert.
Implémentation complète
class AccessibleModal {
constructor(modalElement) {
this.modal = modalElement;
this.overlay = modalElement.previousElementSibling;
this.déclencheur = null;
this.focusables = [];
this.init();
}
init() {
// Boutons de fermeture
this.modal.querySelectorAll('[data-close-modal]').forEach(btn => {
btn.addEventListener('click', () => this.fermer());
});
// Fermeture sur overlay
this.overlay?.addEventListener('click', () => this.fermer());
// Échap pour fermer
this.modal.addEventListener('keydown', (e) => {
if (e.key === 'Escape') this.fermer();
if (e.key === 'Tab') this.gererTab(e);
});
}
ouvrir(déclencheur) {
this.déclencheur = déclencheur;
// Rendre le reste inerte
document.querySelectorAll('body > *:not(.modal-overlay):not(.modal)')
.forEach(el => el.inert = true);
// Afficher
this.overlay.hidden = false;
this.modal.hidden = false;
// Mettre à jour la liste des focusables
this.updateFocusables();
// Focus initial
const premierFocus = this.modal.querySelector('[autofocus]')
|| this.focusables[0]
|| this.modal;
premierFocus.focus();
// Empêcher le scroll du body
document.body.style.overflow = 'hidden';
}
fermer() {
// Masquer
this.modal.hidden = true;
this.overlay.hidden = true;
// Restaurer l'interactivité
document.querySelectorAll('[inert]').forEach(el => el.inert = false);
// Restaurer le scroll
document.body.style.overflow = '';
// Retour du focus
this.déclencheur?.focus();
this.déclencheur = null;
}
updateFocusables() {
this.focusables = Array.from(
this.modal.querySelectorAll(
'a[href], button:not([disabled]), input:not([disabled]), ' +
'select:not([disabled]), textarea:not([disabled]), ' +
'[tabindex]:not([tabindex="-1"])'
)
);
}
gererTab(event) {
if (event.key !== 'Tab') return;
if (this.focusables.length === 0) return;
const premier = this.focusables[0];
const dernier = this.focusables[this.focusables.length - 1];
if (event.shiftKey && document.activeElement === premier) {
event.preventDefault();
dernier.focus();
} else if (!event.shiftKey && document.activeElement === dernier) {
event.preventDefault();
premier.focus();
}
}
}
// Utilisation
const modal = new AccessibleModal(document.querySelector('.modal'));
document.querySelector('.btn-ouvrir').addEventListener('click', (e) => {
modal.ouvrir(e.currentTarget);
});
L'élément dialog natif
L'élément HTML <dialog> gère nativement plusieurs aspects :
<dialog id="ma-modal">
<h2>Titre</h2>
<p>Contenu</p>
<button onclick="this.closest('dialog').close()">Fermer</button>
</dialog>
<button onclick="document.getElementById('ma-modal').showModal()">
Ouvrir
</button>
La méthode showModal() :
- Piège automatiquement le focus
- Rend le contenu extérieur inerte
- Ferme avec Échap
- Fournit le
::backdrop
Consultez notre guide modal vs dialog pour plus de détails.
Checklist de validation
Testez votre modale avec cette liste :
- Focus initial : Le focus se déplace dans la modale à l'ouverture ?
- Focus trap : Impossible de sortir de la modale au Tab ?
- Échap : La modale se ferme avec la touche Échap ?
- Retour focus : Le focus revient au déclencheur ?
- aria-labelledby : La modale a un titre accessible ?
- Contenu inerte : Le contenu arrière-plan est-il vraiment inactif ?
- Lecteur d'écran : "Dialogue" est annoncé correctement ?
- Bouton fermer : Visible et accessible au clavier ?
Lancez un audit RGAA complet pour vérifier l'accessibilité de l'ensemble de votre site.
Conclusion
L'accessibilité des modales repose sur trois piliers : les attributs ARIA corrects, une gestion rigoureuse du focus et l'isolation du contenu arrière-plan. En implémentant ces techniques, vous garantissez une expérience utilisable pour tous, y compris les personnes qui naviguent au clavier ou avec un lecteur d'écran.
L'élément <dialog> natif simplifie grandement cette implémentation. Privilégiez-le quand le support navigateur le permet, et n'hésitez pas à ajouter le polyfill pour les navigateurs plus anciens.
Guides RGAA associés
Pour aller plus loin sur les sujets abordés dans cet article, consultez nos fiches techniques :
Chaque champ de formulaire doit avoir une étiquette (label) qui lui est liée explicitement.
Chaque image porteuse d'information doit avoir une alternative textuelle pertinente via l'attribut alt. Les images décoratives doivent avoir un attribut alt vide.
L'intitulé de chaque lien doit être explicite et permettre de comprendre la destination ou la fonction du lien, même hors contexte.
Articles similaires
Votre site est-il conforme ?
Ne prenez pas de risques avec l'accessibilité. Lancez un audit complet de votre site en quelques minutes et obtenez un rapport détaillé des corrections à apporter.