Aller au contenu principal

Web frontend · L2 · Section 3/6

Accessibilité (A11y)

Progression

Points d’expérience : XPSérie de jours consécutifs : · —Progression du module : — / —compris

#Accessibilité (A11y)

L'accessibilité n'est pas une option morale : c'est un critère de qualité mesurable. Une interface utilisable au clavier, annoncée correctement aux lecteurs d'écran, contrastée et respectueuse des préférences système est aussi plus robuste pour tout le monde : navigation clavier, formulaires tolérants, états visibles. Ce chapitre vous donne les vérifications concrètes dans l'ordre où elles rapportent le plus.

#Les trois réflexes qui corrigent 80 % des problèmes

  1. Tout parcourir au clavier. Posez la souris, naviguez à la touche Tab. Chaque interaction doit être atteignable, l'ordre du focus doit suivre l'ordre visuel, et le focus doit être visible. Si vous ne voyez pas où vous êtes, l'utilisateur non plus.
  2. Coupler libellés et champs. Chaque input a son label for, chaque bouton icône a un libellé (aria-label ou texte visuellement masqué), chaque image a un alt adapté à son rôle.
  3. Annoncer les changements. Erreurs de formulaire, fin de chargement, mise à jour d'un compteur : tout ce qui change sans recharger la page doit vivre dans une région aria-live ou être signalé par le déplacement du focus.

Une boite de dialogue concentre tous les pièges. Voici le cycle à respecter, étape par étape :

Le bouton « Ouvrir » déclenche l'affichage du modal. On mémorise document.activeElement pour pouvoir y revenir : c'est l'élément déclencheur.

Étape 1 / 5

L'attribut inert (désormais bien supporté) rend une région non focusable, non cliquable et invisible aux technologies d'assistance : c'est le moyen le plus sûr de masquer l'arrière-plan d'un modal.

#Structure minimale

htmlhtml

1<button id="open-settings">Paramètres d'affichage</button>2 3<div id="settings" role="dialog" aria-modal="true" aria-labelledby="settings-title" hidden>4  <h2 id="settings-title">Paramètres d'affichage</h2>5  <button aria-label="Fermer la fenêtre">×</button>6  <form>7    <label for="density">Densité</label>8    <select id="density">9      <option value="compact">Compacte</option>10      <option value="cosy">Confortable</option>11    </select>12    <button>Valider</button>13  </form>14</div>
Le JavaScript du cycle (commenté)
let opener = null

function openDialog() {
opener = document.activeElement          // 1. mémoriser
const dlg = document.getElementById('settings')
dlg.hidden = false                        // 2. afficher
document.querySelectorAll('body > *:not(#settings)')
  .forEach(el => el.inert = true)         // 3. figer l'arrière-plan
dlg.querySelector('select, button:not([aria-label])')
  ?.focus()                               // 4. focus utile
}

function closeDialog() {
const dlg = document.getElementById('settings')
dlg.hidden = true
document.querySelectorAll('[inert]')
  .forEach(el => el.inert = false)        // 5. rendre la page
opener?.focus()                           // 6. restituer le focus
}

document.getElementById('open-settings')
.addEventListener('click', openDialog)

document.getElementById('settings')
.addEventListener('keydown', (e) => {
  if (e.key === 'Escape') closeDialog()
  if (e.key !== 'Tab') return
  // Focus trap : boucler dans le dialog
  const items = [...e.currentTarget.querySelectorAll(
    'button, select, input, textarea, a[href]')]
  const first = items[0], last = items.at(-1)
  if (e.shiftKey && document.activeElement === first) {
    e.preventDefault(); last.focus()
  } else if (!e.shiftKey && document.activeElement === last) {
    e.preventDefault(); first.focus()
  }
})

Avec inert sur l'arrière-plan, le piège à focus Tab/Shift+Tab devient une sécurité de plus ; le vrai verrou est déjà posé.

#Contraste, zoom et préférences

  • Contraste : 4.5:1 minimum pour du texte courant, 3:1 pour du texte grand (18.66px gras ou 24px) et les bordures de composants interactifs. Vérifiez avec le sélecteur de couleur de la DevTools, qui affiche le ratio.
  • Zoom : votre page doit rester utilisable à 200 % de zoom et se reflow à 320px de large (une colonne, pas de défilement horizontal). C'est le critère WCAG 1.4.10.
  • Mouvement réduit : enveloppez les animations dans @media (prefers-reduced-motion: reduce) et ne le faites pas qu'en CSS si l'animation vient de JS (matchMedia('(prefers-reduced-motion: reduce)')).
  • Ordre de lecture : le DOM est la voix du lecteur d'écran. Ne « corrigez » pas l'ordre visuel avec order en CSS sans vérifier ce que donne l'ordre du DOM.

#Erreurs fréquentes à éviter

  • Des div cliquables sans rôle ni gestion clavier : utilisez button, le travail est déjà fait.
  • Des placeholders à la place de labels : ils disparaissent à la saisie, ont un contraste faible et ne s'annoncent pas toujours.
  • Des messages d'erreur uniquement colorés : ajoutez le texte et le lien aria-describedby.
  • Des animations lourdes non désactivables malgré prefers-reduced-motion.
  • Supprimer le contour de focus (outline: none) sans replacement visible : c'est retirer le clignotant d'une voiture.

#Exercice

Reprenez le modal ci-dessus et vérifiez-le en conditions réelles : navigation clavier seule du premier au dernier contrôle, Échap pour fermer, et constat que le focus revient au bouton d'ouverture. Auditez ensuite avec l'onglet Accessibility de la DevTools (nom, rôle, valeur du dialog) puis avec l'extension axe DevTools pour chasser les violations restantes.

Preuve attendue : une liste de trois observations (« Échap ferme et restitue le focus », « aria-modal annoncé », « aucune violation axe critique ») plutôt qu'un simple « ça marche ».

Un utilisateur ferme un modal à la souris après l'avoir ouvert au clavier. Que doit-il se passer pour ne pas le perdre ?
Un utilisateur ferme un modal à la souris après l'avoir ouvert au clavier. Que doit-il se passer pour ne pas le perdre ?