Guide de référence complet pour l’accessibilité web : standards WCAG 2.2, ARIA, HTML sémantique, navigation clavier, lecteurs d’écran et outils de test.
Table des matières
- Introduction WCAG 2.2
- HTML sémantique
- ARIA (Accessible Rich Internet Applications)
- Navigation clavier
- Formulaires accessibles
- Images et médias
- Couleurs et contrastes
- Typographie et lisibilité
- Lecteurs d’écran
- Navigation et structure
- Animations et interactions
- Outils de test et validation
Introduction WCAG 2.2
Principes POUR (WCAG)
1. Perceptible : L’information doit être présentée de façon à être perçue par tous 2. Opérable : Les composants de l’interface doivent être utilisables 3. Compréhensible : L’information et l’interface doivent être compréhensibles 4. Robuste : Le contenu doit être compatible avec les technologies d’assistance
Niveaux de conformité
| Niveau | Description | Usage |
|---|---|---|
| A | Niveau minimal | Exigences de base |
| AA | Niveau recommandé | Standard professionnel (requis légalement dans beaucoup de pays) |
| AAA | Niveau optimal | Excellence en accessibilité |
Nouveautés WCAG 2.2 (2023)
Nouveaux critères :
- 2.4.11 Focus Not Obscured (Minimum) – AA
- 2.4.12 Focus Not Obscured (Enhanced) – AAA
- 2.4.13 Focus Appearance – AAA
- 2.5.7 Dragging Movements – AA
- 2.5.8 Target Size (Minimum) – AA
- 3.2.6 Consistent Help – A
- 3.3.7 Redundant Entry – A
- 3.3.8 Accessible Authentication (Minimum) – AA
- 3.3.9 Accessible Authentication (Enhanced) – AAA
HTML sémantique
Structure de page sémantique
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Titre descriptif de la page</title>
</head>
<body>
<!-- Skip link (lien d'évitement) -->
<a href="#main-content" class="skip-link">Aller au contenu principal</a>
<!-- En-tête du site -->
<header role="banner">
<div class="logo">
<img src="logo.svg" alt="Logo de l'entreprise">
</div>
<!-- Navigation principale -->
<nav role="navigation" aria-label="Navigation principale">
<ul>
<li><a href="/">Accueil</a></li>
<li><a href="/services">Services</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
</header>
<!-- Contenu principal -->
<main id="main-content" role="main">
<!-- Article ou section -->
<article>
<h1>Titre principal de la page</h1>
<section aria-labelledby="section1-title">
<h2 id="section1-title">Première section</h2>
<p>Contenu de la section...</p>
</section>
<section aria-labelledby="section2-title">
<h2 id="section2-title">Deuxième section</h2>
<p>Contenu de la section...</p>
</section>
</article>
<!-- Contenu complémentaire -->
<aside role="complementary" aria-label="Informations complémentaires">
<h2>Articles connexes</h2>
<ul>
<li><a href="/article1">Article 1</a></li>
<li><a href="/article2">Article 2</a></li>
</ul>
</aside>
</main>
<!-- Pied de page -->
<footer role="contentinfo">
<p>© 2025 Entreprise. Tous droits réservés.</p>
<nav aria-label="Navigation secondaire">
<ul>
<li><a href="/mentions-legales">Mentions légales</a></li>
<li><a href="/confidentialite">Confidentialité</a></li>
</ul>
</nav>
</footer>
</body>
</html>
Balises sémantiques HTML5
<!-- Navigation -->
<nav aria-label="Navigation principale">
<ul>
<li><a href="/">Accueil</a></li>
</ul>
</nav>
<!-- En-tête -->
<header>
<h1>Titre du site</h1>
</header>
<!-- Contenu principal -->
<main>
<!-- Contenu unique de la page -->
</main>
<!-- Article autonome -->
<article>
<h2>Titre de l'article</h2>
<p>Contenu de l'article...</p>
</article>
<!-- Section thématique -->
<section aria-labelledby="section-title">
<h2 id="section-title">Titre de section</h2>
<p>Contenu...</p>
</section>
<!-- Contenu complémentaire -->
<aside>
<h2>Informations connexes</h2>
</aside>
<!-- Pied de page -->
<footer>
<p>Informations de contact</p>
</footer>
<!-- Figure avec légende -->
<figure>
<img src="chart.png" alt="Graphique des ventes 2024">
<figcaption>Évolution des ventes sur l'année 2024</figcaption>
</figure>
<!-- Définitions -->
<dl>
<dt>HTML</dt>
<dd>HyperText Markup Language</dd>
<dt>CSS</dt>
<dd>Cascading Style Sheets</dd>
</dl>
<!-- Marquage sémantique inline -->
<p>
Ce texte contient un <strong>mot important</strong> et un
<em>mot accentué</em>. Le code s'écrit avec <code><code></code>.
</p>
<!-- Abréviations -->
<abbr title="World Wide Web Consortium">W3C</abbr>
<!-- Citations -->
<blockquote cite="https://source.com">
<p>Citation longue...</p>
<footer>- <cite>Auteur</cite></footer>
</blockquote>
<!-- Heure/Date -->
<time datetime="2025-01-15">15 janvier 2025</time>
Landmarks ARIA
<!-- Banner (en-tête principal) -->
<header role="banner">
<!-- Ou simplement <header> si unique dans la page -->
</header>
<!-- Navigation -->
<nav role="navigation" aria-label="Navigation principale">
<!-- Ou simplement <nav> avec aria-label -->
</nav>
<!-- Main (contenu principal) -->
<main role="main">
<!-- Ou simplement <main> -->
</main>
<!-- Complementary (contenu complémentaire) -->
<aside role="complementary">
<!-- Ou simplement <aside> -->
</aside>
<!-- Contentinfo (pied de page) -->
<footer role="contentinfo">
<!-- Ou simplement <footer> si à la racine -->
</footer>
<!-- Search (zone de recherche) -->
<div role="search">
<form>
<input type="search" aria-label="Rechercher">
<button type="submit">Rechercher</button>
</form>
</div>
<!-- Region (section importante) -->
<section role="region" aria-labelledby="region-title">
<h2 id="region-title">Titre de la région</h2>
</section>
Checklist HTML sémantique
- [ ] Balises HTML5 sémantiques utilisées (header, nav, main, article, aside, footer)
- [ ] Un seul
<main>par page - [ ] Un seul
<h1>par page - [ ] Hiérarchie des titres respectée (h1 → h2 → h3…)
- [ ] Landmarks ARIA appropriés
- [ ] Attribut
langsur<html> - [ ] Pas de balises dépréciées (font, center, etc.)
- [ ] HTML valide (W3C validator)
ARIA (Accessible Rich Internet Applications)
Règles d’or ARIA
- N’utilisez pas ARIA si HTML sémantique suffit
- Ne changez pas la sémantique native
- Tous les contrôles interactifs doivent être utilisables au clavier
- Ne pas utiliser role= »presentation » ou aria-hidden sur éléments focusables
- Tous les éléments interactifs doivent avoir un nom accessible
Rôles ARIA essentiels
<!-- Button -->
<div role="button" tabindex="0" aria-pressed="false">
Bouton personnalisé
</div>
<!-- Préférer : <button>Bouton</button> -->
<!-- Link -->
<div role="link" tabindex="0">Lien personnalisé</div>
<!-- Préférer : <a href="...">Lien</a> -->
<!-- Checkbox -->
<div role="checkbox" aria-checked="false" tabindex="0">
Case à cocher
</div>
<!-- Préférer : <input type="checkbox"> -->
<!-- Radio -->
<div role="radio" aria-checked="false" tabindex="0">
Bouton radio
</div>
<!-- Préférer : <input type="radio"> -->
<!-- Tablist / Tab / Tabpanel -->
<div role="tablist" aria-label="Onglets de contenu">
<button role="tab" aria-selected="true" aria-controls="panel1" id="tab1">
Onglet 1
</button>
<button role="tab" aria-selected="false" aria-controls="panel2" id="tab2">
Onglet 2
</button>
</div>
<div role="tabpanel" id="panel1" aria-labelledby="tab1">
Contenu onglet 1
</div>
<div role="tabpanel" id="panel2" aria-labelledby="tab2" hidden>
Contenu onglet 2
</div>
<!-- Dialog (modale) -->
<div role="dialog" aria-labelledby="dialog-title" aria-modal="true">
<h2 id="dialog-title">Titre de la modale</h2>
<p>Contenu de la modale</p>
<button>Fermer</button>
</div>
<!-- Alert -->
<div role="alert" aria-live="assertive">
Message d'alerte important
</div>
<!-- Status -->
<div role="status" aria-live="polite" aria-atomic="true">
Chargement en cours...
</div>
<!-- Menu / Menuitem -->
<div role="menu" aria-label="Options">
<div role="menuitem">Option 1</div>
<div role="menuitem">Option 2</div>
<div role="menuitem">Option 3</div>
</div>
<!-- Progressbar -->
<div role="progressbar" aria-valuenow="75" aria-valuemin="0" aria-valuemax="100">
75%
</div>
<!-- Slider -->
<div role="slider" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100" tabindex="0">
Slider
</div>
Propriétés ARIA
<!-- aria-label : nom accessible -->
<button aria-label="Fermer la fenêtre">
<span aria-hidden="true">×</span>
</button>
<!-- aria-labelledby : référence à un label -->
<h2 id="section-title">Titre de section</h2>
<div aria-labelledby="section-title">
Contenu de la section
</div>
<!-- aria-describedby : description supplémentaire -->
<input
type="text"
id="username"
aria-describedby="username-help"
>
<span id="username-help">
Entre 3 et 20 caractères
</span>
<!-- aria-hidden : cacher aux lecteurs d'écran -->
<span aria-hidden="true">★</span>
<span class="sr-only">Note : 4 étoiles sur 5</span>
<!-- aria-live : région dynamique -->
<div aria-live="polite" aria-atomic="true">
<!-- Contenu qui se met à jour -->
</div>
<!-- polite = attendre la fin de la lecture -->
<!-- assertive = interrompre immédiatement -->
<!-- aria-current : élément actuel -->
<nav>
<a href="/" aria-current="page">Accueil</a>
<a href="/about">À propos</a>
<a href="/contact">Contact</a>
</nav>
<!-- aria-expanded : état déplié/replié -->
<button aria-expanded="false" aria-controls="menu">
Menu
</button>
<ul id="menu" hidden>
<li>Item 1</li>
<li>Item 2</li>
</ul>
<!-- aria-pressed : bouton toggle -->
<button aria-pressed="false">
Mode sombre
</button>
<!-- aria-checked : état coché -->
<div role="checkbox" aria-checked="true">
Accepter les conditions
</div>
<!-- aria-selected : élément sélectionné -->
<div role="tab" aria-selected="true">
Onglet actif
</div>
<!-- aria-disabled : désactivé -->
<button aria-disabled="true">
Bouton désactivé
</button>
<!-- Préférer : <button disabled> -->
<!-- aria-required : champ requis -->
<input
type="text"
aria-required="true"
>
<!-- Préférer : <input required> -->
<!-- aria-invalid : validation -->
<input
type="email"
aria-invalid="true"
aria-describedby="email-error"
>
<span id="email-error" role="alert">
Format d'email invalide
</span>
<!-- aria-haspopup : indique un popup -->
<button aria-haspopup="menu" aria-expanded="false">
Options
</button>
Live Regions (contenu dynamique)
<!-- Notification non-critique -->
<div role="status" aria-live="polite" aria-atomic="true">
<p>Message enregistré avec succès</p>
</div>
<!-- Alerte importante -->
<div role="alert" aria-live="assertive" aria-atomic="true">
<p>Erreur : veuillez remplir tous les champs obligatoires</p>
</div>
<!-- Timer / Compteur -->
<div aria-live="off" aria-atomic="true" id="timer">
00:30
</div>
<!-- Loading state -->
<div role="status" aria-live="polite" aria-busy="true">
<span>Chargement en cours...</span>
</div>
<!-- Résultats de recherche -->
<div role="region" aria-live="polite" aria-atomic="true">
<p>42 résultats trouvés</p>
</div>
Checklist ARIA
- [ ] ARIA utilisé uniquement si HTML sémantique insuffisant
- [ ] Tous les contrôles ARIA utilisables au clavier
- [ ] États ARIA mis à jour dynamiquement (aria-expanded, aria-pressed, etc.)
- [ ] Live regions pour contenu dynamique
- [ ] aria-label ou aria-labelledby sur éléments interactifs
- [ ] aria-hidden sur éléments purement décoratifs
- [ ] Pas de role= »presentation » sur éléments focusables
- [ ] Validation ARIA (axe DevTools, WAVE)
Navigation clavier
Ordre de tabulation
<!-- Ordre naturel du DOM (recommandé) -->
<button>Bouton 1</button>
<button>Bouton 2</button>
<button>Bouton 3</button>
<!-- tabindex="0" : inclure dans ordre naturel -->
<div role="button" tabindex="0">
Bouton personnalisé
</div>
<!-- tabindex="-1" : focusable par JS, pas par Tab -->
<div tabindex="-1" id="error-message">
Message d'erreur
</div>
<!-- ÉVITER : tabindex positif (ordre imprévisible) -->
<button tabindex="1">Ne pas faire</button>
<button tabindex="2">Mauvaise pratique</button>
Raccourcis clavier
| Touche | Action standard |
|---|---|
| Tab | Élément suivant |
| Shift + Tab | Élément précédent |
| Enter | Activer lien/bouton |
| Espace | Activer bouton, toggle checkbox |
| Flèches | Navigation dans menus, tabs, sliders |
| Échap | Fermer dialog/menu |
| Home | Début de liste/contenu |
| End | Fin de liste/contenu |
| Page Up/Down | Scroll par page |
Navigation au clavier : implémentation
<!-- Skip link -->
<style>
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: #000;
color: #fff;
padding: 8px;
z-index: 100;
}
.skip-link:focus {
top: 0;
}
</style>
<a href="#main-content" class="skip-link">
Aller au contenu principal
</a>
<!-- Menu déroulant accessible clavier -->
<nav>
<button
aria-expanded="false"
aria-controls="submenu"
aria-haspopup="true"
>
Menu
</button>
<ul id="submenu" hidden>
<li><a href="/page1">Page 1</a></li>
<li><a href="/page2">Page 2</a></li>
</ul>
</nav>
<script>
const menuButton = document.querySelector('[aria-controls="submenu"]');
const submenu = document.getElementById('submenu');
menuButton.addEventListener('click', toggleMenu);
menuButton.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
toggleMenu();
}
if (e.key === 'Escape') {
closeMenu();
}
});
function toggleMenu() {
const isExpanded = menuButton.getAttribute('aria-expanded') === 'true';
menuButton.setAttribute('aria-expanded', !isExpanded);
submenu.hidden = isExpanded;
if (!isExpanded) {
submenu.querySelector('a')?.focus();
}
}
function closeMenu() {
menuButton.setAttribute('aria-expanded', 'false');
submenu.hidden = true;
menuButton.focus();
}
</script>
Focus visible
/* Reset du outline natif (MAUVAIS) */
*:focus {
outline: none; /* NE PAS FAIRE */
}
/* Style de focus personnalisé (BON) */
*:focus {
outline: 2px solid #0066cc;
outline-offset: 2px;
}
/* Focus visible uniquement clavier (moderne) */
*:focus-visible {
outline: 2px solid #0066cc;
outline-offset: 2px;
}
/* Désactiver focus visible pour souris */
*:focus:not(:focus-visible) {
outline: none;
}
/* Skip link focus */
.skip-link:focus {
position: absolute;
top: 0;
left: 0;
background: #000;
color: #fff;
padding: 8px 16px;
text-decoration: none;
z-index: 9999;
}
/* Focus dans composants personnalisés */
[role="button"]:focus,
[role="link"]:focus {
outline: 2px solid #0066cc;
outline-offset: 2px;
}
Piège clavier (focus trap dans modales)
// Piéger le focus dans une modale
class FocusTrap {
constructor(element) {
this.element = element;
this.focusableElements = element.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
this.firstFocusable = this.focusableElements[0];
this.lastFocusable = this.focusableElements[this.focusableElements.length - 1];
}
activate() {
this.element.addEventListener('keydown', this.handleKeyDown.bind(this));
this.firstFocusable?.focus();
}
deactivate() {
this.element.removeEventListener('keydown', this.handleKeyDown.bind(this));
}
handleKeyDown(e) {
if (e.key === 'Tab') {
if (e.shiftKey) {
// Shift + Tab
if (document.activeElement === this.firstFocusable) {
e.preventDefault();
this.lastFocusable?.focus();
}
} else {
// Tab
if (document.activeElement === this.lastFocusable) {
e.preventDefault();
this.firstFocusable?.focus();
}
}
}
if (e.key === 'Escape') {
this.close();
}
}
close() {
// Logique de fermeture
this.deactivate();
}
}
// Utilisation
const modal = document.querySelector('[role="dialog"]');
const focusTrap = new FocusTrap(modal);
focusTrap.activate();
Checklist Navigation clavier
- [ ] Tous les éléments interactifs accessibles au clavier
- [ ] Ordre de tabulation logique
- [ ] Focus visible sur tous les éléments focusables
- [ ] Skip link présent
- [ ] Pas de piège clavier (sauf modales)
- [ ] Focus retourne à l’élément déclencheur après fermeture modale
- [ ] Raccourcis clavier documentés si personnalisés
- [ ] Pas d’utilisation de tabindex positif
- [ ] Flèches fonctionnent dans composants appropriés (menus, sliders)
- [ ] Échap ferme les overlays/modales
Formulaires accessibles
Structure de formulaire accessible
<form action="/submit" method="post">
<!-- Label associé à input -->
<div>
<label for="name">Nom complet <abbr title="requis" aria-label="requis">*</abbr></label>
<input
type="text"
id="name"
name="name"
required
aria-required="true"
autocomplete="name"
>
</div>
<!-- Input avec description -->
<div>
<label for="email">Email</label>
<input
type="email"
id="email"
name="email"
required
aria-required="true"
aria-describedby="email-help"
autocomplete="email"
>
<span id="email-help" class="help-text">
Nous ne partagerons jamais votre email
</span>
</div>
<!-- Input avec erreur -->
<div>
<label for="phone">Téléphone</label>
<input
type="tel"
id="phone"
name="phone"
aria-invalid="true"
aria-describedby="phone-error"
autocomplete="tel"
>
<span id="phone-error" role="alert" class="error">
Format invalide. Utilisez : 06 12 34 56 78
</span>
</div>
<!-- Groupe de radio buttons -->
<fieldset>
<legend>Choisissez votre abonnement</legend>
<div>
<input type="radio" id="basic" name="plan" value="basic">
<label for="basic">Basic (9€/mois)</label>
</div>
<div>
<input type="radio" id="pro" name="plan" value="pro">
<label for="pro">Pro (19€/mois)</label>
</div>
<div>
<input type="radio" id="enterprise" name="plan" value="enterprise">
<label for="enterprise">Enterprise (49€/mois)</label>
</div>
</fieldset>
<!-- Groupe de checkboxes -->
<fieldset>
<legend>Préférences de notification</legend>
<div>
<input type="checkbox" id="email-notif" name="notifications" value="email">
<label for="email-notif">Email</label>
</div>
<div>
<input type="checkbox" id="sms-notif" name="notifications" value="sms">
<label for="sms-notif">SMS</label>
</div>
</fieldset>
<!-- Select -->
<div>
<label for="country">Pays</label>
<select id="country" name="country" required autocomplete="country">
<option value="">-- Sélectionnez un pays --</option>
<option value="BE">Belgique</option>
<option value="CH">Suisse</option>
</select>
</div>
<!-- Textarea -->
<div>
<label for="message">Message</label>
<textarea
id="message"
name="message"
rows="5"
aria-describedby="message-help"
maxlength="500"
></textarea>
<span id="message-help">
Maximum 500 caractères
</span>
</div>
<!-- Boutons -->
<div>
<button type="submit">Envoyer</button>
<button type="reset">Réinitialiser</button>
<a href="/" role="button">Annuler</a>
</div>
</form>
Validation accessible
<!-- Message d'erreur global -->
<div role="alert" aria-live="assertive" id="form-errors">
<h2>Erreurs dans le formulaire</h2>
<ul>
<li><a href="#name">Le nom est requis</a></li>
<li><a href="#email">L'email est invalide</a></li>
</ul>
</div>
<!-- Champ avec erreur -->
<div class="form-field error">
<label for="email">Email</label>
<input
type="email"
id="email"
name="email"
aria-invalid="true"
aria-describedby="email-error"
class="error"
>
<span id="email-error" role="alert">
Veuillez entrer un email valide
</span>
</div>
<!-- Champ valide -->
<div class="form-field success">
<label for="username">Nom d'utilisateur</label>
<input
type="text"
id="username"
name="username"
aria-invalid="false"
aria-describedby="username-success"
>
<span id="username-success" role="status">
✓ Nom d'utilisateur disponible
</span>
</div>
Autocomplete
<!-- Attributs autocomplete appropriés -->
<input type="text" name="name" autocomplete="name">
<input type="email" name="email" autocomplete="email">
<input type="tel" name="phone" autocomplete="tel">
<input type="text" name="organization" autocomplete="organization">
<input type="text" name="street" autocomplete="street-address">
<input type="text" name="city" autocomplete="address-level2">
<input type="text" name="postal" autocomplete="postal-code">
<input type="text" name="country" autocomplete="country">
<input type="text" name="cc-number" autocomplete="cc-number">
<input type="text" name="cc-exp" autocomplete="cc-exp">
Checklist Formulaires
- [ ] Tous les inputs ont un label associé (for/id)
- [ ] Labels visibles (pas uniquement placeholder)
- [ ] Champs requis indiqués visuellement et programmatiquement
- [ ] Erreurs de validation claires et spécifiques
- [ ] Erreurs annoncées aux lecteurs d’écran (role= »alert »)
- [ ] aria-invalid sur champs en erreur
- [ ] aria-describedby pour aide et erreurs
- [ ] Fieldset/legend pour groupes de champs
- [ ] Autocomplete approprié
- [ ] Focus sur premier champ en erreur après soumission
- [ ] Bouton submit clairement identifié
Images et médias
Images accessibles
<!-- Image informative -->
<img
src="chart.png"
alt="Graphique montrant une augmentation de 25% des ventes en 2024"
width="800"
height="600"
>
<!-- Image décorative -->
<img src="decoration.png" alt="" role="presentation">
<!-- Image avec légende -->
<figure>
<img
src="product.jpg"
alt="Smartphone noir avec écran OLED de 6,5 pouces"
>
<figcaption>
Nouveau modèle X - Disponible en 3 couleurs
</figcaption>
</figure>
<!-- Image complexe (avec longdesc alternative) -->
<img
src="complex-chart.png"
alt="Graphique des tendances de marché 2024"
aria-describedby="chart-description"
>
<div id="chart-description" class="sr-only">
<p>Description détaillée : Le graphique montre...</p>
</div>
<!-- SVG accessible -->
<svg role="img" aria-labelledby="svg-title">
<title id="svg-title">Icône de validation</title>
<path d="..."/>
</svg>
<!-- SVG décoratif -->
<svg aria-hidden="true" focusable="false">
<path d="..."/>
</svg>
<!-- Image map -->
<img
src="map.png"
alt="Plan du campus"
usemap="#campus-map"
>
<map name="campus-map">
<area
shape="rect"
coords="0,0,100,100"
href="/building-a"
alt="Bâtiment A - Administration"
>
<area
shape="rect"
coords="100,0,200,100"
href="/building-b"
alt="Bâtiment B - Bibliothèque"
>
</map>
<!-- Lazy loading -->
<img
src="image.jpg"
alt="Description de l'image"
loading="lazy"
width="800"
height="600"
>
Vidéos accessibles
<!-- Vidéo avec sous-titres et transcription -->
<figure>
<video controls width="640" height="360">
<source src="video.mp4" type="video/mp4">
<source src="video.webm" type="video/webm">
<!-- Sous-titres (captions) -->
<track
kind="captions"
src="captions-fr.vtt"
srclang="fr"
label="Français"
default
>
<track
kind="captions"
src="captions-en.vtt"
srclang="en"
label="English"
>
<!-- Descriptions audio -->
<track
kind="descriptions"
src="descriptions-fr.vtt"
srclang="fr"
label="Descriptions français"
>
<!-- Chapitres -->
<track
kind="chapters"
src="chapters.vtt"
srclang="fr"
label="Chapitres"
>
<!-- Fallback -->
<p>
Votre navigateur ne supporte pas la vidéo HTML5.
<a href="video.mp4">Télécharger la vidéo</a>
</p>
</video>
<figcaption>
<details>
<summary>Transcription de la vidéo</summary>
<p>Transcription textuelle complète de la vidéo...</p>
</details>
</figcaption>
</figure>
<!-- Format WebVTT (captions-fr.vtt) -->
<!--
WEBVTT
00:00:00.000 --> 00:00:03.000
Bienvenue dans cette vidéo tutoriel.
00:00:03.000 --> 00:00:07.000
Aujourd'hui, nous allons apprendre l'accessibilité web.
-->
Audio accessible
<figure>
<audio controls>
<source src="podcast.mp3" type="audio/mpeg">
<source src="podcast.ogg" type="audio/ogg">
<!-- Transcription obligatoire pour audio -->
<p>
Votre navigateur ne supporte pas l'élément audio.
<a href="podcast.mp3">Télécharger le podcast</a>
</p>
</audio>
<figcaption>
<details>
<summary>Transcription de l'audio</summary>
<p>Transcription complète du contenu audio...</p>
</details>
</figcaption>
</figure>
Checklist Médias
- [ ] Toutes les images ont un attribut alt approprié
- [ ] Images décoratives avec alt= » » ou role= »presentation »
- [ ] Vidéos ont des sous-titres (captions)
- [ ] Vidéos ont une transcription textuelle
- [ ] Audio a une transcription textuelle
- [ ] Contrôles média accessibles au clavier
- [ ] Pas de lecture automatique (autoplay)
- [ ] SVG ont title ou aria-label si informatifs
- [ ] Width et height définis sur images (CLS)
Couleurs et contrastes
Ratios de contraste WCAG 2.2
| Élément | Niveau AA | Niveau AAA |
|---|---|---|
| Texte normal (< 18pt) | 4.5:1 | 7:1 |
| Texte large (≥ 18pt ou ≥ 14pt gras) | 3:1 | 4.5:1 |
| Composants UI et graphiques | 3:1 | – |
| Texte de logo | Pas de minimum | – |
Exemples de contrastes
/* BON : Contraste 7:1 (AAA) */
.text-high-contrast {
color: #000000; /* Noir */
background-color: #ffffff; /* Blanc */
}
/* BON : Contraste 4.52:1 (AA pour texte normal) */
.text-good-contrast {
color: #595959; /* Gris foncé */
background-color: #ffffff; /* Blanc */
}
/* INSUFFISANT : Contraste 2.9:1 */
.text-poor-contrast {
color: #999999; /* Gris */
background-color: #ffffff; /* Blanc */
/* Ne passe pas AA */
}
/* BON : Texte large avec contraste 3:1 */
.large-text {
font-size: 18pt;
font-weight: bold;
color: #767676;
background-color: #ffffff;
}
/* Boutons et composants UI : 3:1 minimum */
.button {
background-color: #0066cc;
border: 2px solid #004080; /* Contraste 3.2:1 avec background */
}
/* Focus indicator : 3:1 minimum */
*:focus-visible {
outline: 2px solid #0066cc; /* Contraste avec arrière-plan */
outline-offset: 2px;
}
Ne pas utiliser uniquement la couleur
<!-- MAUVAIS : information uniquement par couleur -->
<p style="color: red;">Champ requis</p>
<!-- BON : combinaison couleur + texte/icône -->
<p>
<span style="color: red;">*</span>
<span>Champ requis</span>
</p>
<!-- BON : états de formulaire -->
<div class="form-field error">
<label for="email">Email</label>
<input type="email" id="email" aria-invalid="true">
<span class="error-icon" aria-hidden="true">❌</span>
<span class="error-message">Email invalide</span>
</div>
<!-- BON : graphique avec motifs -->
<svg role="img" aria-label="Graphique des ventes">
<!-- Barres avec différentes couleurs ET motifs -->
<rect fill="blue" />
<rect fill="red" pattern="url(#stripes)" />
</svg>
<!-- BON : liens distinguables sans couleur -->
<style>
a {
color: #0066cc;
text-decoration: underline; /* Toujours souligné */
}
</style>
Mode sombre accessible
/* Variables CSS pour thème */
:root {
--text-primary: #000000;
--background-primary: #ffffff;
--link-color: #0066cc;
}
@media (prefers-color-scheme: dark) {
:root {
--text-primary: #ffffff;
--background-primary: #1a1a1a;
--link-color: #66b3ff; /* Plus clair pour bon contraste */
}
}
/* Utilisation */
body {
color: var(--text-primary);
background-color: var(--background-primary);
}
a {
color: var(--link-color);
}
/* Toggle manuel */
[data-theme="dark"] {
--text-primary: #ffffff;
--background-primary: #1a1a1a;
--link-color: #66b3ff;
}
Checklist Couleurs
- [ ] Contraste texte normal ≥ 4.5:1 (AA)
- [ ] Contraste texte large ≥ 3:1 (AA)
- [ ] Contraste composants UI ≥ 3:1
- [ ] Information pas uniquement par couleur
- [ ] Liens distinguables (underline ou bold)
- [ ] Focus visible avec bon contraste
- [ ] Mode sombre accessible si présent
- [ ] Palette testée (outils contraste)
Typographie et lisibilité
Taille de texte
/* Texte de base : minimum 16px */
body {
font-size: 16px;
line-height: 1.5; /* 150% minimum */
}
/* Texte peut être zoomé jusqu'à 200% sans perte de fonctionnalité */
/* Utiliser unités relatives (rem, em) */
h1 {
font-size: 2rem; /* 32px si base = 16px */
}
p {
font-size: 1rem; /* 16px */
line-height: 1.5; /* 24px */
}
/* Texte large pour accessibilité */
.large-text {
font-size: 1.125rem; /* 18px */
line-height: 1.6;
}
/* ÉVITER : taille fixe en pixels pour texte */
/* Empêche le zoom texte du navigateur */
Espacement
/* Espacement pour lisibilité (WCAG 2.2 - 1.4.12) */
* {
/* Line height : minimum 1.5x font-size */
line-height: 1.5;
/* Espacement paragraphes : minimum 2x font-size */
margin-bottom: 2em;
/* Letter spacing : minimum 0.12x font-size */
letter-spacing: 0.12em;
/* Word spacing : minimum 0.16x font-size */
word-spacing: 0.16em;
}
/* Largeur de ligne optimale */
p {
max-width: 70ch; /* 70 caractères maximum par ligne */
}
Polices lisibles
/* Polices sans-serif pour écrans */
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif;
}
/* Éviter polices trop stylisées pour contenu principal */
.decorative-heading {
font-family: 'Fancy Font', cursive;
/* OK pour titres courts, pas pour paragraphes */
}
/* Dyslexie : polices spécialisées */
.dyslexia-friendly {
font-family: 'OpenDyslexic', 'Comic Sans MS', sans-serif;
}
Justification et alignement
/* Éviter justification complète (difficile à lire) */
.text-block {
text-align: left; /* BON */
/* text-align: justify; */ /* ÉVITER pour accessibilité */
}
/* Césure contrôlée */
p {
hyphens: auto;
overflow-wrap: break-word;
}
Checklist Typographie
- [ ] Taille de texte base ≥ 16px
- [ ] Line-height ≥ 1.5
- [ ] Texte zoomable jusqu’à 200%
- [ ] Unités relatives (rem, em, %)
- [ ] Largeur de ligne ≤ 80 caractères
- [ ] Espacement paragraphes suffisant
- [ ] Pas de justification pour longs textes
- [ ] Police lisible
- [ ] Pas de texte en image (sauf logo)
Lecteurs d’écran
Screen reader only (SR-only)
/* Classe pour texte visible uniquement par lecteurs d'écran */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* Visible au focus (skip links) */
.sr-only-focusable:focus {
position: static;
width: auto;
height: auto;
overflow: visible;
clip: auto;
white-space: normal;
}
Exemples d’utilisation
<!-- Contexte supplémentaire pour SR -->
<button>
<span aria-hidden="true">×</span>
<span class="sr-only">Fermer la fenêtre</span>
</button>
<!-- Information visuelle explicitée -->
<a href="/cart">
Panier
<span class="badge">3</span>
<span class="sr-only">3 articles dans le panier</span>
</a>
<!-- États visuels -->
<div class="status-icon success" aria-hidden="true">✓</div>
<span class="sr-only">Opération réussie</span>
<!-- Navigation par landmarks -->
<nav aria-label="Navigation principale">
<span class="sr-only">Début de la navigation principale</span>
<ul>
<li><a href="/">Accueil</a></li>
</ul>
<span class="sr-only">Fin de la navigation principale</span>
</nav>
Texte alternatif de qualité
<!-- BON : descriptif et concis -->
<img src="dog.jpg" alt="Golden retriever jouant avec une balle rouge dans un parc">
<!-- MAUVAIS : redondant -->
<img src="photo.jpg" alt="Photo d'un golden retriever jouant avec une balle rouge dans un parc">
<!-- MAUVAIS : trop vague -->
<img src="image.jpg" alt="Image">
<!-- BON : contexte du lien -->
<a href="/article">
<img src="thumb.jpg" alt="Lire l'article : 10 astuces SEO">
</a>
<!-- MAUVAIS : "cliquez ici" -->
<a href="/article">
<img src="thumb.jpg" alt="Cliquez ici">
</a>
Ordre de lecture
<!-- L'ordre DOM = ordre de lecture -->
<!-- BON : ordre logique -->
<article>
<h2>Titre de l'article</h2>
<p class="date">15 janvier 2025</p>
<p>Contenu de l'article...</p>
<a href="/lire-plus">Lire la suite</a>
</article>
<!-- MAUVAIS : ordre visuel ≠ ordre DOM -->
<article>
<p style="order: 3">Contenu...</p>
<h2 style="order: 1">Titre</h2>
<p class="date" style="order: 2">Date</p>
<!-- Lecteur d'écran lit dans ordre DOM, pas ordre visuel -->
</article>
Checklist Lecteurs d’écran
- [ ] Contenu important pas dans aria-hidden
- [ ] Textes alternatifs descriptifs
- [ ] Ordre DOM logique
- [ ] .sr-only pour contexte supplémentaire
- [ ] aria-label sur éléments sans label visible
- [ ] Landmarks ARIA appropriés
- [ ] Live regions pour contenu dynamique
- [ ] Test avec lecteur d’écran (NVDA, JAWS, VoiceOver)
Navigation et structure
Fil d’Ariane (Breadcrumb)
<nav aria-label="Fil d'Ariane">
<ol>
<li>
<a href="/">Accueil</a>
</li>
<li>
<a href="/categorie">Catégorie</a>
</li>
<li>
<a href="/sous-categorie">Sous-catégorie</a>
</li>
<li aria-current="page">
Page actuelle
</li>
</ol>
</nav>
Pagination
<nav aria-label="Pagination" role="navigation">
<ul>
<li>
<a href="/page/1" aria-label="Aller à la page 1">
<span aria-hidden="true">‹‹</span>
<span class="sr-only">Première page</span>
</a>
</li>
<li>
<a href="/page/4" aria-label="Page précédente">
<span aria-hidden="true">‹</span>
<span class="sr-only">Précédent</span>
</a>
</li>
<li>
<a href="/page/3">3</a>
</li>
<li>
<a href="/page/4">4</a>
</li>
<li aria-current="page">
<span class="current-page">
5
<span class="sr-only">(page actuelle)</span>
</span>
</li>
<li>
<a href="/page/6">6</a>
</li>
<li>
<a href="/page/7">7</a>
</li>
<li>
<a href="/page/6" aria-label="Page suivante">
<span aria-hidden="true">›</span>
<span class="sr-only">Suivant</span>
</a>
</li>
<li>
<a href="/page/10" aria-label="Aller à la dernière page (page 10)">
<span aria-hidden="true">››</span>
<span class="sr-only">Dernière page</span>
</a>
</li>
</ul>
</nav>
Table des matières
<nav aria-labelledby="toc-heading">
<h2 id="toc-heading">Table des matières</h2>
<ol>
<li>
<a href="#section1">Introduction</a>
</li>
<li>
<a href="#section2">Méthodes</a>
<ol>
<li><a href="#section2-1">Méthode A</a></li>
<li><a href="#section2-2">Méthode B</a></li>
</ol>
</li>
<li>
<a href="#section3">Conclusion</a>
</li>
</ol>
</nav>
Checklist Navigation
- [ ] Navigation principale identifiée (nav + aria-label)
- [ ] Skip links présents
- [ ] Fil d’Ariane si applicable
- [ ] Pagination accessible
- [ ] Lien actuel identifié (aria-current= »page »)
- [ ] Landmarks ARIA (banner, main, navigation, etc.)
- [ ] Table des matières pour longs articles
- [ ] Tous les liens ont texte descriptif
Animations et interactions
Respecter prefers-reduced-motion
/* Animations par défaut */
.element {
transition: transform 0.3s ease;
}
.element:hover {
transform: scale(1.1);
}
/* Désactiver pour utilisateurs sensibles */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
/* Alternative : animations subtiles */
@media (prefers-reduced-motion: reduce) {
.element {
transition: opacity 0.3s ease; /* OK */
/* transition: transform 0.3s ease; */ /* Éviter */
}
}
Timeout et limites de temps
<!-- Avertir l'utilisateur avant timeout -->
<div role="alert" aria-live="assertive" id="session-warning" hidden>
<p>
Votre session expirera dans <span id="countdown">60</span> secondes.
<button onclick="extendSession()">Prolonger la session</button>
</p>
</div>
<script>
let timeoutDuration = 1800000; // 30 minutes
let warningTime = 60000; // 1 minute avant
function startSessionTimer() {
setTimeout(() => {
showWarning();
}, timeoutDuration - warningTime);
}
function showWarning() {
document.getElementById('session-warning').hidden = false;
startCountdown(60);
}
function extendSession() {
document.getElementById('session-warning').hidden = true;
startSessionTimer();
}
</script>
Carrousel accessible
<section aria-roledescription="carrousel" aria-label="Produits vedettes">
<!-- Contrôles -->
<div class="carousel-controls">
<button
aria-label="Diapositive précédente"
onclick="previousSlide()"
>
‹
</button>
<button
aria-label="Pause automatique"
aria-pressed="false"
id="pause-button"
onclick="toggleAutoplay()"
>
Pause
</button>
<button
aria-label="Diapositive suivante"
onclick="nextSlide()"
>
›
</button>
</div>
<!-- Slides -->
<div class="carousel-slides">
<div
class="slide"
role="group"
aria-roledescription="diapositive"
aria-label="1 sur 4"
aria-hidden="false"
>
<img src="product1.jpg" alt="Description produit 1">
<p>Produit 1</p>
</div>
<div
class="slide"
role="group"
aria-roledescription="diapositive"
aria-label="2 sur 4"
aria-hidden="true"
hidden
>
<img src="product2.jpg" alt="Description produit 2">
<p>Produit 2</p>
</div>
<!-- Autres slides... -->
</div>
<!-- Indicateurs -->
<div class="carousel-indicators" role="tablist">
<button
role="tab"
aria-label="Diapositive 1"
aria-selected="true"
onclick="goToSlide(0)"
></button>
<button
role="tab"
aria-label="Diapositive 2"
aria-selected="false"
onclick="goToSlide(1)"
></button>
</div>
</section>
Checklist Animations
- [ ] prefers-reduced-motion respecté
- [ ] Pas d’animations automatiques critiques
- [ ] Contrôle pause/lecture pour animations
- [ ] Timeouts avec avertissement et extension possible
- [ ] Pas de clignotement > 3 fois/seconde
- [ ] Carrousels avec contrôles accessibles
- [ ] Auto-scroll désactivable
Outils de test et validation
Outils automatiques
| Outil | Type | URL |
|---|---|---|
| WAVE | Extension navigateur | https://wave.webaim.org/ |
| axe DevTools | Extension navigateur | https://www.deque.com/axe/ |
| Lighthouse | Chrome DevTools | Intégré Chrome |
| Pa11y | CLI | https://pa11y.org/ |
| Accessibility Insights | Extension + Desktop | https://accessibilityinsights.io/ |
| ARC Toolkit | Extension Chrome | https://www.tpgi.com/arc-platform/arc-toolkit/ |
Outils de contraste
| Outil | Usage |
|---|---|
| WebAIM Contrast Checker | https://webaim.org/resources/contrastchecker/ |
| Colour Contrast Analyser | Application desktop |
| Stark | Plugin Figma/Sketch |
Lecteurs d’écran
| Lecteur | Plateforme | Navigateur |
|---|---|---|
| NVDA | Windows | Firefox, Chrome |
| JAWS | Windows | Chrome, Edge |
| VoiceOver | macOS/iOS | Safari |
| Narrator | Windows | Edge |
| TalkBack | Android | Chrome |
Checklist de tests
Tests automatiques
- [ ] WAVE : 0 erreur
- [ ] axe DevTools : 0 erreur
- [ ] Lighthouse Accessibility : score > 90
- [ ] HTML valide (W3C validator)
- [ ] Contraste vérifié (tous textes/composants)
Tests manuels
- [ ] Navigation clavier complète
- [ ] Tous les éléments interactifs accessibles
- [ ] Ordre de tabulation logique
- [ ] Focus visible sur tous éléments
- [ ] Skip links fonctionnels
- [ ] Formulaires testés avec validation
- [ ] Modales/overlays avec focus trap
- [ ] Zoom texte 200% fonctionnel
Tests lecteurs d’écran
- [ ] Test NVDA (Windows)
- [ ] Test VoiceOver (macOS/iOS)
- [ ] Landmarks correctement annoncés
- [ ] Titres correctement hiérarchisés
- [ ] Images alt text appropriés
- [ ] Formulaires labels annoncés
- [ ] Live regions fonctionnelles
- [ ] États ARIA annoncés (expanded, pressed, etc.)
Tests utilisateurs
- [ ] Tests avec utilisateurs réels en situation de handicap
- [ ] Tests sur différents devices
- [ ] Tests avec assistances technologies variées
Checklist WCAG 2.2 complète
Niveau A (minimum)
Perceptible
- [ ] 1.1.1 Contenu non textuel (alt text)
- [ ] 1.2.1 Audio et vidéo en différé (alternative)
- [ ] 1.2.2 Sous-titres (pré-enregistré)
- [ ] 1.2.3 Audio-description (pré-enregistré)
- [ ] 1.3.1 Info et relations (sémantique HTML)
- [ ] 1.3.2 Ordre séquentiel logique
- [ ] 1.3.3 Caractéristiques sensorielles
- [ ] 1.4.1 Utilisation de la couleur
- [ ] 1.4.2 Contrôle du son
Opérable
- [ ] 2.1.1 Clavier
- [ ] 2.1.2 Pas de piège clavier
- [ ] 2.1.4 Raccourcis clavier
- [ ] 2.2.1 Réglage du délai
- [ ] 2.2.2 Mettre en pause, arrêter, masquer
- [ ] 2.3.1 Pas de clignotement
- [ ] 2.4.1 Contourner des blocs (skip links)
- [ ] 2.4.2 Titre de page
- [ ] 2.4.3 Parcours du focus
- [ ] 2.4.4 Fonction du lien (contexte)
- [ ] 2.5.1 Gestes pour le pointeur
- [ ] 2.5.2 Annulation de l’action du pointeur
- [ ] 2.5.3 Étiquette dans le nom
- [ ] 2.5.4 Activation par le mouvement
Compréhensible
- [ ] 3.1.1 Langue de la page
- [ ] 3.2.1 Au focus
- [ ] 3.2.2 À la saisie
- [ ] 3.2.6 Aide cohérente (nouveau 2.2)
- [ ] 3.3.1 Identification des erreurs
- [ ] 3.3.2 Étiquettes ou instructions
- [ ] 3.3.7 Saisie redondante (nouveau 2.2)
Robuste
- [ ] 4.1.1 Analyse syntaxique (déprécié en 2.2)
- [ ] 4.1.2 Nom, rôle et valeur
- [ ] 4.1.3 Messages de statut
Niveau AA (recommandé)
Perceptible
- [ ] 1.2.4 Sous-titres (en direct)
- [ ] 1.2.5 Audio-description (pré-enregistré)
- [ ] 1.3.4 Orientation
- [ ] 1.3.5 Identification de la finalité
- [ ] 1.4.3 Contraste minimum (4.5:1)
- [ ] 1.4.4 Redimensionnement du texte
- [ ] 1.4.5 Texte sous forme d’image
- [ ] 1.4.10 Reflow
- [ ] 1.4.11 Contraste non textuel
- [ ] 1.4.12 Espacement du texte
- [ ] 1.4.13 Contenu au survol ou au focus
Opérable
- [ ] 2.4.5 Accès multiples
- [ ] 2.4.6 En-têtes et étiquettes
- [ ] 2.4.7 Visibilité du focus
- [ ] 2.4.11 Focus non obscurci (minimum) (nouveau 2.2)
- [ ] 2.5.7 Mouvements de glissement (nouveau 2.2)
- [ ] 2.5.8 Taille de la cible (minimum) (nouveau 2.2)
Compréhensible
- [ ] 3.1.2 Langue d’un passage
- [ ] 3.2.3 Navigation cohérente
- [ ] 3.2.4 Identification cohérente
- [ ] 3.3.3 Suggestion d’erreur
- [ ] 3.3.4 Prévention des erreurs (juridique, financier)
- [ ] 3.3.8 Authentification accessible (minimum) (nouveau 2.2)
Niveau AAA (optimal)
Perceptible
- [ ] 1.2.6 Langue des signes (pré-enregistré)
- [ ] 1.2.7 Audio-description étendue
- [ ] 1.2.8 Média de remplacement
- [ ] 1.2.9 Audio seulement (en direct)
- [ ] 1.4.6 Contraste amélioré (7:1)
- [ ] 1.4.7 Fond sonore faible ou absent
- [ ] 1.4.8 Présentation visuelle
- [ ] 1.4.9 Texte sous forme d’image (sans exception)
Opérable
- [ ] 2.1.3 Clavier (sans exception)
- [ ] 2.2.3 Pas de délai
- [ ] 2.2.4 Interruptions
- [ ] 2.2.5 Re-authentification
- [ ] 2.2.6 Délais d’expiration
- [ ] 2.3.2 Trois flashs
- [ ] 2.3.3 Animations dues aux interactions
- [ ] 2.4.8 Localisation
- [ ] 2.4.9 Fonction du lien (seul)
- [ ] 2.4.10 En-têtes de section
- [ ] 2.4.12 Focus non obscurci (amélioré) (nouveau 2.2)
- [ ] 2.4.13 Apparence du focus (nouveau 2.2)
- [ ] 2.5.5 Taille de la cible (amélioré)
- [ ] 2.5.6 Mécanismes de saisie
Compréhensible
- [ ] 3.1.3 Mots inhabituels
- [ ] 3.1.4 Abréviations
- [ ] 3.1.5 Niveau de lecture
- [ ] 3.1.6 Prononciation
- [ ] 3.2.5 Changement à la demande
- [ ] 3.3.5 Aide
- [ ] 3.3.6 Prévention des erreurs (toutes)
- [ ] 3.3.9 Authentification accessible (amélioré) (nouveau 2.2)
Ressources
Documentation officielle
- WCAG 2.2 : https://www.w3.org/WAI/WCAG22/quickref/
- WAI-ARIA : https://www.w3.org/WAI/ARIA/apg/
- MDN Accessibility : https://developer.mozilla.org/en-US/docs/Web/Accessibility
Guides et tutoriels
- WebAIM : https://webaim.org/
- The A11Y Project : https://www.a11yproject.com/
- Deque University : https://dequeuniversity.com/
Législation
- Union européenne : Directive (UE) 2016/2102 et norme EN 301 549
- Europe : European Accessibility Act
- USA : ADA (Americans with Disabilities Act), Section 508
Version : WCAG 2.2 (octobre 2023) Dernière mise à jour : Décembre 2024
Ce guide couvre les standards WCAG 2.2 et les meilleures pratiques d’accessibilité web pour créer des expériences inclusives pour tous les utilisateurs.