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 lang="fr">Multilingue (Razor / Umbraco)
<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
| Balise | Rô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
<div class="header"> ... </div>
<div class="title-big">Titre principal</div>À faire
<header> ... </header>
<h1>Titre principal</h1>3. Images et alt
Règle simple
- Image informative :
altdécrit ce qu'elle apporte au contenu - Image décorative :
alt=""(vide, mais présent) - Image liée à une action (logo cliquable) :
altdécrit la destination
Mauvais
<img src="team.jpg" />
<img src="deco-line.svg" alt="deco-line" />
<img src="logo.svg" alt="logo" />Bon
<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 :
<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
<div class="btn" onclick="openModal()">Ouvrir</div>
<a href="#" onclick="doSomething()">Clic</a>Bon
<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
<a href="/artisans/martin">Cliquez ici</a>À faire
<a href="/artisans/martin">Découvrir le travail de Martin</a>Si le design impose "En savoir plus", ajouter un libellé caché :
<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
<button class="btn-trigger-cart" data-title="Shop">
<svg>...</svg>
</button>Bon
<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) :
<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 :
.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
<input type="text" placeholder="Prénom" />Le placeholder disparaît à la saisie et est souvent en contraste faible.
Bon
<label for="firstname">Prénom</label>
<input type="text" id="firstname" name="firstname" />Avec Razor (pattern metiers-d-art)
<label asp-for="FirstName">Prénom*</label>
<input asp-for="FirstName" required />Champs requis
Le * visuel ne suffit pas. Combiner :
<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 :
<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
*:focus { outline: none; }
button:focus { outline: 0; }Bon
: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
| Attribut | Usage |
|---|---|
aria-label | Donner un nom à un élément sans texte (bouton SVG) |
aria-labelledby | Référencer un autre élément comme label |
aria-describedby | Référencer un texte d'aide |
aria-expanded | État ouvert/fermé (accordéon, dropdown) |
aria-controls | Lien 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)
<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)
<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 :
<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-expandedentretrueetfalse - toggle l'attribut
hiddensur 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"
<dialog id="contact" aria-labelledby="dialog-title">
<h2 id="dialog-title">Contact</h2>
<button autofocus>Fermer</button>
...
</dialog>10. Skip link (lien d'évitement)
Permet à un utilisateur clavier de sauter le menu pour aller au contenu. Premier élément du <body> :
<a href="#main" class="skip-link">Aller au contenu</a>.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 :
- https://webaim.org/resources/contrastchecker/
- DevTools Chrome : inspecter le texte => onglet "Accessibilité" => ratio affiché
- Extension "Stark" (Figma/Chrome)
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é
Navigateur seul (2 minutes)
- Naviguer toute la page au clavier (
Tab,Shift+Tab,Enter,Espace,Esc) - Vérifier que le focus est toujours visible
- Vérifier que tous les boutons/liens sont atteignables
Outils automatiques (couvrent env. 30 % des problèmes)
| Outil | Où |
|---|---|
| Lighthouse | DevTools Chrome => onglet Lighthouse => Accessibility |
| axe DevTools | Extension Chrome/Firefox |
| WAVE | https://wave.webaim.org |
Lecteur d'écran (test réel)
- Windows : NVDA (gratuit, https://www.nvaccess.org)
- Mac : VoiceOver (
Cmd+F5) - Mobile : TalkBack (Android), VoiceOver (iOS)
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-labelou<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é

