Skip to content

Accessibilité web (a11y)

Ce guide explique comment rendre un site accessible sans se noyer dans la théorie WCAG. On vise le pragmatique : les règles qui se tiennent dans une review de PR, avec des exemples tirés de nos projets

Cas typique : sur pully-website, le bouton panier n'a que l'icône SVG et un data-title="Shop". Un lecteur d'écran annonce juste "bouton" - impossible de savoir ce que c'est. Fix : ajouter un <span class="visually-hidden">Panier</span> dans le bouton. 15 secondes de travail, utile pour les utilisateurs qui naviguent au clavier ou au lecteur d'écran (env. 5-10 % du trafic selon les sites)

L'objectif : un site utilisable au clavier, annonçable correctement par un lecteur d'écran, sans surcoût de dev une fois les réflexes acquis.

1. Langue du document

Obligatoire sur <html>, sinon le lecteur d'écran lit le français avec un accent anglais (et inversement).

Bon

html
<html lang="fr">

Multilingue (Razor / Umbraco)

html
<html lang="@currentLangIso">

Vu dans metiers-d-art _MasterLayout.cshtml:29 -> pattern à reprendre systématiquement.

2. Structure sémantique

Un navigateur s'en fout, un lecteur d'écran non. Les landmarks permettent de sauter directement à une zone.

Landmarks obligatoires

BaliseRôle
<header>En-tête du site
<nav>Navigation principale
<main id="main">Contenu principal (un seul par page)
<footer>Pied de page
<aside>Contenu annexe

Hiérarchie des titres

Une seule <h1> par page. Pas de saut de niveau (h2 => h4 sans h3). L'ordre doit refléter la logique du contenu, pas le design.

À éviter

html
<div class="header"> ... </div>
<div class="title-big">Titre principal</div>

À faire

html
<header> ... </header>
<h1>Titre principal</h1>

3. Images et alt

Règle simple

  • Image informative : alt décrit ce qu'elle apporte au contenu
  • Image décorative : alt="" (vide, mais présent)
  • Image liée à une action (logo cliquable) : alt décrit la destination

Mauvais

html
<img src="team.jpg" />
<img src="deco-line.svg" alt="deco-line" />
<img src="logo.svg" alt="logo" />

Bon

html
<img src="team.jpg" alt="L'équipe Spektrum Media en réunion" />
<img src="deco-line.svg" alt="" />
<img src="logo.svg" alt="Accueil Spektrum Media" />

Cas Umbraco / Razor

Beaucoup de nos projets utilisent un helper .GetAltText() (vu dans apol-website imageBlock.cshtml). Vérifier qu'il ne retourne jamais null et que le champ alt est remplissable dans le back-office. Sinon fallback sur le nom de fichier (pas idéal mais mieux que rien) - pattern metiers-d-art artisanCard.cshtml:44 :

html
<img alt="@listingImage?.Name" src="..." loading="lazy" />

4. Liens et boutons

La règle

  • Lien (<a href>) : navigue vers une autre page/ancre
  • Bouton (<button>) : déclenche une action (ouvrir modale, envoyer form, toggle menu)

Un <div onclick> n'est ni l'un ni l'autre : pas focusable au clavier, pas annonçable.

Mauvais

html
<div class="btn" onclick="openModal()">Ouvrir</div>
<a href="#" onclick="doSomething()">Clic</a>

Bon

html
<button type="button" onclick="openModal()">Ouvrir</button>
<a href="/contact">Contact</a>

Libellés de liens

"Cliquez ici" ou "En savoir plus" pris hors contexte ne disent rien. Un lecteur d'écran peut lister tous les liens d'une page - s'ils s'appellent tous "En savoir plus", c'est inutilisable.

À éviter

html
<a href="/artisans/martin">Cliquez ici</a>

À faire

html
<a href="/artisans/martin">Découvrir le travail de Martin</a>

Si le design impose "En savoir plus", ajouter un libellé caché :

html
<a href="/artisans/martin">
  En savoir plus
  <span class="visually-hidden"> sur Martin</span>
</a>

SVG icons dans un bouton

Cas fréquent : bouton panier, menu burger, fermer modale. Le SVG seul n'est pas un texte.

Mauvais

html
<button class="btn-trigger-cart" data-title="Shop">
  <svg>...</svg>
</button>

Bon

html
<button class="btn-trigger-cart" type="button" aria-label="Panier">
  <svg aria-hidden="true">...</svg>
</button>

Ou avec texte masqué visuellement (préféré, car plus robuste à la traduction) :

html
<button class="btn-trigger-cart" type="button">
  <svg aria-hidden="true">...</svg>
  <span class="visually-hidden">Panier</span>
</button>

Classe utilitaire visually-hidden

À avoir dans chaque projet :

css
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

5. Formulaires

Chaque champ doit avoir un <label>

Mauvais

html
<input type="text" placeholder="Prénom" />

Le placeholder disparaît à la saisie et est souvent en contraste faible.

Bon

html
<label for="firstname">Prénom</label>
<input type="text" id="firstname" name="firstname" />

Avec Razor (pattern metiers-d-art)

html
<label asp-for="FirstName">Prénom*</label>
<input asp-for="FirstName" required />

Champs requis

Le * visuel ne suffit pas. Combiner :

html
<input asp-for="Email"
       type="email"
       required
       aria-required="true"
       aria-describedby="email-help" />
<span id="email-help">Format : nom@domaine.ch</span>

Messages d'erreur

Associer le message au champ via aria-describedby ou aria-invalid :

html
<input type="email" aria-invalid="true" aria-describedby="email-error" />
<span id="email-error" role="alert">Adresse invalide</span>

6. Focus clavier (piège courant)

Le plus grand péché a11y de nos projets : Bootstrap reset les outlines (outline: 0) et on oublie de remettre un style de focus.

Test rapide : naviguer la page uniquement avec Tab. Si on ne voit pas où est le curseur, c'est cassé.

Mauvais

css
*:focus { outline: none; }
button:focus { outline: 0; }

Bon

css
:focus-visible {
  outline: 2px solid #0066cc;
  outline-offset: 2px;
}

:focus-visible n'affiche le style que pour la navigation clavier, pas pour les clics souris - meilleur des deux mondes.

7. ARIA : moins, c'est mieux

Règle n°1 d'ARIA : pas d'ARIA vaut mieux qu'un ARIA faux. Une balise sémantique correcte (<button>, <nav>) est toujours préférable à un <div role="button">.

Les seuls ARIA utiles au quotidien

AttributUsage
aria-labelDonner un nom à un élément sans texte (bouton SVG)
aria-labelledbyRéférencer un autre élément comme label
aria-describedbyRéférencer un texte d'aide
aria-expandedÉtat ouvert/fermé (accordéon, dropdown)
aria-controlsLien entre bouton et zone contrôlée
aria-hidden="true"Cacher au lecteur d'écran (icônes déco)
aria-current="page"Page active dans le menu
role="alert"Annoncer un message (erreur form)

Pattern accordéon (repris de metiers-d-art accordionBlock.cshtml:21)

html
<button aria-expanded="false" aria-controls="panel-1">
  Titre section
</button>
<div id="panel-1" hidden>
  Contenu
</div>

Le JS bascule aria-expanded et hidden. Rien d'autre à ajouter.

Pattern dropdown menu (apol-website _header.cshtml:66)

html
<a id="dropdownMenu" role="button" data-bs-toggle="dropdown" aria-expanded="false">
  Menu
</a>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu">
  ...
</ul>

Note : préférer <button> à <a role="button"> quand il n'y a pas de vraie navigation.

8. Menu burger mobile

Le pattern minimal :

html
<button type="button"
        class="btn-hamburger"
        aria-expanded="false"
        aria-controls="main-nav"
        aria-label="Menu">
  <span class="hamburger-box" aria-hidden="true">...</span>
</button>
<nav id="main-nav" hidden>
  ...
</nav>

En JS, au click :

  • toggle aria-expanded entre true et false
  • toggle l'attribut hidden sur la nav

9. Modales / dialogs

Utiliser <dialog> natif quand possible (supporté partout depuis 2022). Sinon :

  • piéger le focus dans la modale tant qu'elle est ouverte
  • restaurer le focus sur le bouton déclencheur à la fermeture
  • fermer avec Esc
  • ajouter role="dialog" + aria-labelledby (titre) + aria-modal="true"
html
<dialog id="contact" aria-labelledby="dialog-title">
  <h2 id="dialog-title">Contact</h2>
  <button autofocus>Fermer</button>
  ...
</dialog>

Permet à un utilisateur clavier de sauter le menu pour aller au contenu. Premier élément du <body> :

html
<a href="#main" class="skip-link">Aller au contenu</a>
css
.skip-link {
  position: absolute;
  top: -40px;
  left: 0;
  background: #000;
  color: #fff;
  padding: 8px 16px;
  z-index: 100;
}
.skip-link:focus {
  top: 0;
}

<main id="main"> doit exister (déjà le cas sur les 3 projets analysés).

11. Contraste des couleurs

Minimum WCAG AA :

  • texte normal : 4.5:1
  • texte large (≥ 18px bold ou 24px regular) : 3:1
  • composants UI (bordure de bouton, icône) : 3:1

Outils :

Ne jamais s'appuyer uniquement sur la couleur pour transmettre une info (erreur en rouge, succès en vert) - ajouter un icône ou un texte.

12. Tester l'accessibilité

  1. Naviguer toute la page au clavier (Tab, Shift+Tab, Enter, Espace, Esc)
  2. Vérifier que le focus est toujours visible
  3. Vérifier que tous les boutons/liens sont atteignables

Outils automatiques (couvrent env. 30 % des problèmes)

Outil
LighthouseDevTools Chrome => onglet Lighthouse => Accessibility
axe DevToolsExtension Chrome/Firefox
WAVEhttps://wave.webaim.org

Lecteur d'écran (test réel)

Tester au moins la navigation et le formulaire principal.

13. Bonnes pratiques

  • lang="xx" sur <html>, toujours
  • Landmarks : <header>, <nav>, <main>, <footer> - une seule <main> par page
  • Une seule <h1>, pas de saut dans la hiérarchie
  • <button> pour action, <a> pour navigation, jamais <div onclick>
  • Alt sur toutes les images (alt="" pour les décoratives)
  • <label for=""> sur tous les inputs, pas juste des placeholders
  • Focus visible : :focus-visible { outline: 2px solid ... }
  • SVG icon seul dans un bouton => aria-label ou <span class="visually-hidden">
  • Ne pas abuser d'ARIA : HTML sémantique d'abord
  • Tester au clavier avant chaque livraison
  • Lancer Lighthouse - viser ≥ 95 sur l'onglet Accessibilité

Contributors

The avatar of contributor named as Clément Favre Clément Favre

Changelog