Niveau 27 — Accessibilité

L'Accessibilité (A11Y)

Rendez vos interfaces utilisables par tous. Du contraste des couleurs au focus clavier, du respect des préférences utilisateur à l'optimisation pour les lecteurs d'écran, l'accessibilité est au cœur du CSS moderne.

1. Comprendre l'accessibilité web (A11Y)

En une phrase : L'accessibilité web (A11Y) consiste à rendre le contenu et les interfaces utilisables par le plus grand nombre, y compris les personnes en situation de handicap (visuel, moteur, cognitif).

Le terme A11Y vient de A + 11 lettres + Y (accessibility). L'accessibilité n'est pas une option : c'est une exigence légale dans de nombreux pays (RGAA en France, WCAG au niveau international, ADA aux USA).

Le CSS joue un rôle fondamental dans l'accessibilité : contraste des couleurs, focus visible, tailles de toucher, respect des préférences utilisateur, et plus encore.

ACCESSIBILITÉ WEB — LES 4 PRINCIPES WCAG
═══════════════════════════════════════════

  1. PERCEPTIBLE ─────── L'information doit être présentable
     • Contraste suffisant (4.5:1 minimum)
     • Alternatives textuelles (alt, sr-only)
     • Labels explicites sur les formulaires
     • Pas d'info uniquement par couleur

  2. UTILISABLE ──────── Les composants doivent être utilisables
     • Navigation au clavier (Tab, Enter, Escape)
     • Focus visible sur tous les éléments interactifs
     • Cibles de toucher ≥ 44×44px
     • Pas de piège clavier (focus trapping contrôlé)

  3. COMPRÉHENSIBLE ──── L'info et l'interface doivent être claires
     • Labels et instructions clairs
     • Navigation prévisible
     • Messages d'erreur explicites

  4. ROBUSTE ─────────── Le contenu doit être interprété
     • HTML sémantique
     • ARIA quand nécessaire
     • Compatible avec les lecteurs d'écran

Les 4 principes fondamentaux des WCAG (Web Content Accessibility Guidelines)

Pourquoi l'accessibilité est essentielle

Besoin Sans A11Y Avec A11Y
Contraste Texte illisible pour malvoyants Rapport ≥ 4.5:1, lisibilité garantie
Navigation clavier Impossible sans souris Tab, Enter, Escape fonctionnent
Focus visible L'utilisateur ne sait pas où il est Indicateur visuel clair de position
Mouvement réduit Vertige, nausée, migraine Animations désactivées ou minimisées
Toucher Cibles trop petites, frustrantes Boutons ≥ 44×44px, faciles à viser
Lecteur d'écran Contenu invisible pour l'assistant Texte sr-only, ARIA, sémantique
Impact : Environ 15% de la population mondiale vit avec un handicap. L'accessibilité profite à tout le monde : personnes âgées, utilisateurs mobiles, connexion lente, environnement bruyant…

2. Syntaxe détaillée

2.1 Contraste des couleurs (WCAG AA/AAA)

Définition : Le contraste entre le texte et son arrière-plan doit respecter des ratios minimums selon les WCAG. Le niveau AA est le minimum légal, le niveau AAA est l'excellence.
RAPPORTS DE CONTRASTE WCAG
══════════════════════════

  NIVEAU AA (minimum requis)          NIVEAU AAA (excellence)
  ─────────────────────────           ────────────────────────
  • Texte normal :   ≥ 4.5:1         • Texte normal :   ≥ 7:1
  • Texte large :    ≥ 3:1           • Texte large :    ≥ 4.5:1
  • Composants UI :  ≥ 3:1           • Composants UI :  Non requis

  "Texte normal" = < 18pt (ou < 14pt gras)
  "Texte large"  = ≥ 18pt (ou ≥ 14pt gras)

  Outils de vérification :
  • Chrome DevTools → onglet Rendering
  • extension "axe DevTools"
  • https://webaim.org/resources/contrastchecker/
  • https://colourcontrast.cc/

Ratios de contraste requis par les WCAG

/* Exemples de contraste */
.texte-suffisant {
  color: #1e293b;        /* Sur fond blanc : ratio 14.5:1 ✅ AAA */
}

.texte-insuffisant {
  color: #94a3b8;        /* Sur fond blanc : ratio 2.8:1 ❌ Échec */
}

.texte-aa {
  color: #595959;        /* Sur fond blanc : ratio 7:1 ✅ AAA */
}

/* Outils CSS pour vérifier */
:root {
  --color-text: #1e293b;
  --color-bg: #ffffff;
  /* Ratio calculé = 14.5:1 → AAA */
}

Vérificateur de contraste

Texte d'exemple — Aa
Ratio : 14.5:1 — ✓ AAA Pass
14.5:1 AAA ✓
7:1 AAA ✓
4.5:1 AA ✓
2.8:1 Échec ✗
5.2:1 AA ✓ (sur fond sombre)
15.3:1 AAA ✓ (dark mode)

2.2 États de focus

Définition : Le focus indique à l'utilisateur quel élément est actuellement sélectionné. C'est indispensable pour la navigation au clavier. Le CSS offre :focus (tout) et :focus-visible (clavier uniquement).
/* :focus — s'applique à tout (clavier + clic) */
.element:focus {
  outline: 2px solid #2563eb;
}

/* :focus-visible — uniquement au clavier (recommandé) */
.element:focus-visible {
  outline: 3px solid #2563eb;
  outline-offset: 2px;
}

/* Supprimer l'outline uniquement pour le clic souris */
.element:focus:not(:focus-visible) {
  outline: none;
}

/* Personnaliser le focus */
.bouton:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px #2563eb, 0 0 0 6px white;
}

/* Focus ring personnalisé */
.input:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px white, 0 0 0 4px #2563eb;
}
Focus classique
focus-visible
Focus ring personnalisé
Double ring (boîte)

2.3 Skip links (liens d'évitement)

Définition : Un skip link est un lien invisible qui apparaît au focus (Tab) en premier élément de la page, permettant de passer directement au contenu principal sans naviguer dans tout le menu.
/* HTML — Premier élément du body */
<a href="#main-content" class="skip-link">Aller au contenu principal</a>

/* CSS */
.skip-link {
  position: absolute;
  top: -100%;
  left: 1rem;
  background: #2563eb;
  color: white;
  padding: 0.5rem 1rem;
  border-radius: 0 0 6px 6px;
  font-weight: 600;
  text-decoration: none;
  z-index: 100;
  transition: top 0.2s;
}

.skip-link:focus {
  top: 0;  /* Apparaît au focus clavier */
}

/* Conteneur principal */
#main-content {
  scroll-margin-top: 1rem;
}

Démo : Skip link (appuyez sur Tab)

2.4 Focus trapping dans les modals

Définition : Le focus trapping (piégeage du focus) empêche la navigation Tab de sortir d'un composant modal. L'utilisateur ne peut naviguer que dans les éléments de la modal jusqu'à sa fermeture.
/* Le focus trapping est principalement géré en JavaScript, */
/* mais le CSS joue un rôle crucial pour l'indication visuelle */

/* Focus visible dans la modal */
.modal [tabindex]:focus-visible {
  outline: 3px solid #2563eb;
  outline-offset: 2px;
}

/* Le backdrop doit empêcher le clic en dehors */
.modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  z-index: 1000;
}

.modal-content {
  position: relative;
  z-index: 1001;
  /* Le tabindex="-1" permet le focus programmatique */
}

Démo : Modal avec focus trap

2.5 Navigation au clavier

/* Ordre de tabulation (Tab order) */
/* L'ordre Tab suit l'ordre DOM, pas l'ordre visuel */
/* Utiliser tabindex="0" pour ajouter un élément au Tab order */
/* Utiliser tabindex="-1" pour le focus programmatique uniquement */
/* NE JAMAIS utiliser tabindex > 0 (désordonne l'ordre) */

/* Styles de focus pour la navigation clavier */
.nav-link:focus-visible {
  outline: 2px solid #2563eb;
  outline-offset: 4px;
}

/* Éléments interactifs必须 être accessibles au clavier */
/* Utiliser <button> ou <a> plutôt que <div onclick> */

/* Simuler :hover au clavier */
.interactive:focus-visible,
.interactive:hover {
  background: #eff6ff;
  transform: translateY(-2px);
}

/* Raccourci clavier visuel */
kbd {
  display: inline-block;
  padding: 0.15rem 0.4rem;
  background: #f1f5f9;
  border: 1px solid #e2e8f0;
  border-radius: 4px;
  font-family: monospace;
  font-size: 0.8em;
  box-shadow: 0 1px 0 #cbd5e1;
}

2.6 Tailles de toucher (min 44×44px)

Définition : Les cibles de toucher (boutons, liens, icônes cliquables) doivent avoir une taille minimale de 44×44 pixels (WCAG 2.2) pour être facilement activables sur les écrans tactiles.
/* Taille minimale de toucher */
.bouton,
.lien-nav,
.icone-cliquable {
  min-width: 44px;
  min-height: 44px;
  /* Ou utiliser padding pour agrandir la zone cliquable */
}

/* Zone cliquable étendue */
.nav-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
  padding: 0.5rem 1rem;
}

/* Icône avec zone de toucher invisible */
.icon-btn {
  position: relative;
  width: 24px;
  height: 24px;
}

.icon-btn::before {
  content: "";
  position: absolute;
  inset: -10px;  /* Zone de toucher étendue */
}
← Les deux premiers ≥ 44px ✓, le dernier trop petit ✗

2.7 Media queries de préférences utilisateur

Définition : Les media queries de préférences permettent d'adapter le CSS aux paramètres de l'utilisateur : schéma de couleur, mouvement réduit, contraste, transparence, et mode forcé.
/* prefers-color-scheme : thème clair/sombre */
@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0f172a;
    --text: #f1f5f9;
  }
}

/* prefers-reduced-motion : animations réduites */
@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;
  }
}

/* prefers-contrast : contraste élevé */
@media (prefers-contrast: more) {
  :root {
    --border: #000000;
    --text: #000000;
    --bg: #ffffff;
  }
}

/* prefers-reduced-transparency */
@media (prefers-reduced-transparency: reduce) {
  .backdrop {
    background: rgba(0, 0, 0, 0.95); /* Plus opaque */
  }
}

/* forced-colors : mode contraste élevé (Windows) */
@media (forced-colors: active) {
  .bouton {
    border: 2px solid ButtonText;
  }
  .icone {
    forced-color-adjust: none; /* Préserver les couleurs */
  }
}

/* inverted-colors */
@media (inverted-colors) {
  .logo {
    filter: invert(1); /* Ré-inverser pour garder le logo original */
  }
}

prefers-color-scheme

Détecte si l'utilisateur préfère le thème clair ou sombre. Utilisez-le pour adapter les couleurs de fond, texte et bordures.

prefers-reduced-motion

Détecte si l'utilisateur souhaite réduire les animations. Désactivez les animations décoratives et réduisez les transitions.

prefers-contrast

Détecte si l'utilisateur souhaite un contraste élevé. Augmentez les contrastes de texte et de bordures.

prefers-reduced-transparency

Détecte si l'utilisateur souhaite réduire la transparence. Remplacez les fonds translucides par des fonds opaques.

forced-colors

Mode contraste élevé du système (Windows HC). Utilisez forced-color-adjust pour préserver les couleurs essentielles.

inverted-colors

Mode couleurs inversées. Protégez les images et logos avec filter: invert(1) pour les ré-inverser.

Démo : prefers-reduced-motion

Élément animé

Si vous avez activé "Réduire le mouvement" dans vos paramètres système, cette animation sera désactivée.

2.8 Texte pour lecteur d'écran (sr-only)

Définition : La classe .sr-only cache visuellement un texte tout en le rendant accessible aux lecteurs d'écran. Elle est essentielle pour les icônes, les boutons d'action et les éléments purement visuels.
/* Classe sr-only — texte accessible visuellement caché */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border-width: 0;
}

/* Utilisation */
<button aria-label="Fermer">✕</button>
<button><span class="sr-only">Fermer le menu</span>☰</button>
<nav aria-label="Menu principal">...</nav>

/* sr-only focusable — visible uniquement au focus */
.sr-only-focusable:focus {
  position: static;
  width: auto;
  height: auto;
  padding: inherit;
  margin: inherit;
  overflow: visible;
  clip: auto;
  white-space: normal;
}

2.9 aria-hidden et aria-label

/* aria-hidden="true" — masque un élément des lecteurs d'écran */
<span aria-hidden="true">🎨</span>  

/* aria-label — étiquette textuelle pour les lecteurs d'écran */
<button aria-label="Rechercher">🔍</button>
<a href="/panier" aria-label="Panier (3 articles)">🛒</a>

/* aria-labelledby — référence un élément comme label */
<h2 id="section-title">Paramètres</h2>
<div aria-labelledby="section-title">...</div>

/* aria-describedby — description supplémentaire */
<input aria-describedby="password-hint">
<span id="password-hint">8 caractères minimum</span>

/* role — définir le rôle d'un élément */
<div role="alert">Erreur !</div>
<div role="status">Chargement...</div>
<div role="progressbar" aria-valuenow="75"></div>

Bouton accessible

Bouton avec aria-label — le lecteur d'écran lit "Supprimer l'élément"

Formulaire accessible

Nous ne partagerons jamais votre adresse.

3. Visualisation

3.1 Hiérarchie des contrastes

CONTRASTE — GUIDE VISUEL
═════════════════════════

  Ratio    Niveau    Usage
  ─────    ──────    ─────
  1:1      —         Invisible (texte sur même fond)
  2:1      —         Insuffisant
  3:1      AA UI     Composants, grosses images
  4.5:1    AA        Texte normal minimum
  7:1      AAA       Texte normal excellent
  21:1     —         Noir sur blanc maximum

  Exemples concrets :
  ┌─────────────────────────────────────────┐
  │  #1e293b sur #ffffff → 14.5:1  AAA  ✅  │
  │  #475569 sur #ffffff →  7.0:1  AAA  ✅  │
  │  #64748b sur #ffffff →  4.6:1  AA   ✅  │
  │  #94a3b8 sur #ffffff →  2.8:1  FAIL ❌  │
  │  #2563eb sur #ffffff →  4.6:1  AA   ✅  │
  │  #ffffff sur #1e293b → 14.5:1  AAA  ✅  │
  └─────────────────────────────────────────┘

Guide de référence pour les ratios de contraste

3.2 Ordre de focus au clavier

NAVIGATION CLAVIER — FLUX DE FOCUS
═══════════════════════════════════

  Page chargée
       │
       ▼
  Skip link (Tab) ──── "Aller au contenu"
       │
       ▼
  Navigation principale
  ├── Logo (Tab)
  ├── Lien 1 (Tab)
  ├── Lien 2 (Tab)
  └── Lien 3 (Tab)
       │
       ▼
  Contenu principal
  ├── Titre
  ├── Formulaire
  │   ├── Input 1 (Tab)
  │   ├── Input 2 (Tab)
  │   └── Bouton submit (Tab)
  └── Liens externes
       │
       ▼
  Footer
  └── Liens footer

  Raccourcis :
  Tab        → Élément suivant
  Shift+Tab  → Élément précédent
  Enter      → Activer un lien/bouton
  Espace     → Activer un bouton/checkbox
  Escape     → Fermer une modal/dropdown
  Flèches    → Naviguer dans un menu/radio

Flux de navigation au clavier d'une page web

3.3 Composants accessibles — Aperçu

Alertes

✓ Opération réussie
ℹ Information importante

Barre de progression

aria-valuenow="65" — le lecteur d'écran lit "65%"

Onglets

Contenu de l'onglet Général — focus clavier avec Tab + flèches.

4. Exemples progressifs

4.1 Composant bouton accessible complet

/* Bouton accessible — toutes les bonnes pratiques */
.btn-a11y {
  /* Taille minimale de toucher */
  min-height: 44px;
  min-width: 44px;

  /* Style de base */
  padding: 0.6rem 1.25rem;
  border: 2px solid #2563eb;
  border-radius: 8px;
  background: #2563eb;
  color: white;
  font-size: 1rem;
  font-weight: 600;
  cursor: pointer;

  /* Transition fluide */
  transition: background 0.2s, box-shadow 0.2s;
}

/* Focus clavier — outline visible */
.btn-a11y:focus-visible {
  outline: 3px solid #2563eb;
  outline-offset: 2px;
  box-shadow: none;
}

/* Focus clic souris — pas d'outline */
.btn-a11y:focus:not(:focus-visible) {
  outline: none;
}

/* Hover */
.btn-a11y:hover {
  background: #1d4ed8;
}

/* État actif */
.btn-a11y:active {
  transform: scale(0.98);
}

/* Désactivé */
.btn-a11y:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* Respect du mouvement réduit */
@media (prefers-reduced-motion: reduce) {
  .btn-a11y { transition: none; }
}

/* Respect du contraste élevé */
@media (prefers-contrast: more) {
  .btn-a11y { border-color: #000; }
}

4.2 Formulaire accessible complet

/* Formulaire — accessibilité maximale */
.form-a11y {
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
  max-width: 400px;
}

.form-group {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}

/* Label toujours visible et associé */
.form-group label {
  font-weight: 600;
  font-size: 0.9rem;
  color: #1e293b;
}

/* Input avec taille de toucher adequate */
.form-group input,
.form-group select {
  padding: 0.6rem 0.75rem;
  border: 2px solid #e2e8f0;
  border-radius: 6px;
  font-size: 1rem;
  min-height: 44px;
  background: white;
  color: #1e293b;
}

/* Focus visible */
.form-group input:focus-visible {
  outline: none;
  border-color: #2563eb;
  box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
}

/* Message d'erreur — role="alert" */
.form-error {
  color: #dc2626;
  font-size: 0.85rem;
  font-weight: 500;
  display: flex;
  align-items: center;
  gap: 0.25rem;
}

/* Indice — aria-describedby */
.form-hint {
  color: #64748b;
  font-size: 0.8rem;
}

/* État d'erreur */
.form-group input[aria-invalid="true"] {
  border-color: #dc2626;
}

.form-group input[aria-invalid="true"]:focus-visible {
  box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.15);
}

4.3 Navigation accessible

/* Navigation — sémantique + focus */
.nav-a11y {
  display: flex;
  gap: 0;
  background: white;
  border-radius: 8px;
  overflow: hidden;
  border: 1px solid #e2e8f0;
}

.nav-a11y a {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 0.75rem 1.25rem;
  text-decoration: none;
  color: #475569;
  font-weight: 500;
  transition: color 0.15s, background 0.15s;
  border: none;
  background: none;
}

.nav-a11y a:hover {
  color: #2563eb;
  background: #eff6ff;
}

.nav-a11y a:focus-visible {
  outline: 2px solid #2563eb;
  outline-offset: -2px;
}

.nav-a11y a[aria-current="page"] {
  color: #2563eb;
  font-weight: 700;
  background: #eff6ff;
}

4.4 Cards accessibles

/* Carte cliquable — entièrement cliquable */
.card-clickable {
  position: relative;
  border: 1px solid #e2e8f0;
  border-radius: 12px;
  overflow: hidden;
  transition: box-shadow 0.2s;
}

.card-clickable:hover {
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}

/* Le lien englobe toute la carte */
.card-clickable a {
  display: block;
  padding: 1.5rem;
  text-decoration: none;
  color: inherit;
}

/* Focus sur toute la carte */
.card-clickable:focus-within {
  outline: 3px solid #2563eb;
  outline-offset: 2px;
}

/* Éviter le double focus sur le lien interne */
.card-clickable a:focus-visible {
  outline: none;
}

4.5 Mode sombre accessible

/* Thème sombre avec contraste vérifié */
:root {
  --bg: #ffffff;
  --text: #1e293b;        /* 14.5:1 sur blanc — AAA */
  --text-secondary: #475569; /* 7:1 sur blanc — AAA */
  --border: #e2e8f0;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #0f172a;
    --text: #f1f5f9;        /* 15.3:1 sur #0f172a — AAA */
    --text-secondary: #cbd5e1; /* 9.2:1 sur #0f172a — AAA */
    --border: #334155;
  }
}

[data-theme="dark"] {
  --bg: #0f172a;
  --text: #f1f5f9;
  --text-secondary: #cbd5e1;
  --border: #334155;
}

body {
  background: var(--bg);
  color: var(--text);
  transition: background 0.3s, color 0.3s;
}

@media (prefers-reduced-motion: reduce) {
  body { transition: none; }
}

4.6 Skip link complet

/* Skip link — implémentation complète */
.skip-link {
  position: absolute;
  top: -100%;
  left: 50%;
  transform: translateX(-50%);
  background: #2563eb;
  color: white;
  padding: 0.75rem 1.5rem;
  border-radius: 0 0 8px 8px;
  font-weight: 600;
  text-decoration: none;
  z-index: 10000;
  transition: top 0.2s ease;
  white-space: nowrap;
}

.skip-link:focus {
  top: 0;
  outline: 3px solid #2563eb;
  outline-offset: 2px;
}

/* Ajouter au début du body */
/* <a href="#main" class="skip-link">Aller au contenu</a> */
/* <main id="main" tabindex="-1">...</main> */

5. Erreurs fréquentes

Erreur 1 : Supprimer l'outline sans alternative
/* FAUX — rend le focus invisible pour les utilisateurs clavier */
*:focus {
  outline: none;
}

/* CORRECT — alternative visuelle au focus */
*:focus:not(:focus-visible) {
  outline: none;
}

*:focus-visible {
  outline: 3px solid #2563eb;
  outline-offset: 2px;
}
Erreur 2 : Contraste insuffisant
/* FAUX — texte gris clair sur fond blanc (2.8:1) */
.texte-secondaire {
  color: #94a3b8;   /* ❌ Échec AA */
}

/* CORRECT — contraste suffisant (7:1) */
.texte-secondaire {
  color: #475569;   /* ✅ AAA */
}
Erreur 3 : Pas de prefers-reduced-motion
/* FAUX — vertige, nausée pour certains utilisateurs */
.animé {
  animation: shake 0.5s ease-in-out infinite;
}

/* CORRECT */
.animé {
  animation: shake 0.5s ease-in-out infinite;
}

@media (prefers-reduced-motion: reduce) {
  .animé {
    animation: none;
  }
}
Erreur 4 : Cibles de toucher trop petites
/* FAUX — bouton 24×24px, impossible à toucher */
.mini-btn {
  width: 24px;
  height: 24px;
}

/* CORRECT — zone de toucher 44×44px */
.bouton {
  min-width: 44px;
  min-height: 44px;
  /* Ou agrandir la zone cliquable */
  padding: 10px;  /* Zone interne plus grande */
}
Erreur 5 : Utiliser div/span au lieu de bouton
/* FAUX — pas de rôle, pas de clavier */
<div class="bouton" onclick="action()">Cliquer</div>

/* CORRECT — élément sémantique */
<button type="button" onclick="action()">Cliquer</button>
Erreur 6 : Masquer le texte d'erreur uniquement par CSS
/* FAUX — le lecteur d'écran ne le lira pas */
.erreur {
  clip: rect(0, 0, 0, 0);
  position: absolute;
}

/* CORRECT — sr-only + role="alert" */
.erreur {
  /* Classe sr-only pour le masquage visuel */
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border-width: 0;
}
Erreur 7 : Information portée uniquement par la couleur
/* FAUX — un daltonien ne voit pas la différence */
.champ-erreur { border-color: red; }
.champ-valide { border-color: green; }

/* CORRECT — couleur + icône/texte */
.champ-erreur {
  border-color: #dc2626;
  border-width: 2px;
}
.champ-erreur::after {
  content: " ✗";
  color: #dc2626;
  font-weight: 700;
}
Erreur 8 : animation infinie sans prefers-reduced-motion
/* RISQUE — vertige, distraction permanente */
.spinner {
  animation: spin 1s linear infinite;
}

/* BON — arrêt quand l'utilisateur le demande */
@media (prefers-reduced-motion: reduce) {
  .spinner {
    /* Garder un état statique */
    border: 3px solid #2563eb;
    border-top-color: transparent;
    border-radius: 50%;
    animation: none;
  }
}

6. Bonnes pratiques

Les règles d'or de l'accessibilité CSS :
  • Toujours fournir un focus visible — utiliser :focus-visible pour les outline personnalisés.
  • Vérifier les contrastes — minimum 4.5:1 pour le texte normal, 3:1 pour le texte large et les UI.
  • Respecter prefers-reduced-motion — désactiver les animations décoratives.
  • Taille de toucher ≥ 44×44px — sur tous les éléments interactifs.
  • Utiliser la classe .sr-only — pour les textes accessibles visuellement cachés.
  • Préférer les éléments sémantiques<button> plutôt que <div onclick>.
  • Ne pas supprimer les outline — les remplacer par des styles de focus personnalisés.
  • Tester au clavier — naviguer uniquement avec Tab, Shift+Tab, Enter, Escape.
Checklist d'accessibilité CSS :
/* Checklist de review a11y */
/* ☐ Tous les éléments interactifs ont un focus-visible */
/* ☐ Le contraste respecte WCAG AA (4.5:1 texte, 3:1 UI) */
/* ☐ prefers-reduced-motion est respecté */
/* ☐ prefers-color-scheme est supporté */
/* ☐ Les cibles de toucher font ≥ 44×44px */
/* ☐ La classe .sr-only est disponible */
/* ☐ Les animations ne déclenchent pas de seizure */
/* ☐ Le mode forcé (forced-colors) est supporté */
/* ☐ L'outline n'est jamais supprimé sans alternative */
/* ☐ Les médias sont adaptés (pas d'info par couleur seule) */

Composants CSS accessibles réutilisables

Bibliothèque de classes utilitaires a11y :
/* === UTILITAIRES D'ACCESSIBILITÉ === */

/* Texte visible uniquement pour les 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-width: 0;
}

/* Visible au focus (pour skip links) */
.sr-only-focusable:focus {
  position: static;
  width: auto;
  height: auto;
  padding: inherit;
  margin: inherit;
  overflow: visible;
  clip: auto;
  white-space: normal;
}

/* Focus visible universel */
:focus-visible {
  outline: 3px solid #2563eb;
  outline-offset: 2px;
}

/* Supprimer l'outline au clic souris uniquement */
:focus:not(:focus-visible) {
  outline: none;
}

/* Réduire les animations */
@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;
  }
}

/* Support du contraste élevé */
@media (prefers-contrast: more) {
  :root {
    --border: #000000;
    --text: #000000;
  }
}

7. Mini-défi

Défi 1 : Skip link parfait

Créez un skip link qui apparaît uniquement au focus Tab, avec un style visible et une animation fluide. Testez qu'il apparaît bien en premier quand vous appuyez sur Tab.

.skip-link {
  position: absolute;
  top: -100%;
  left: 50%;
  transform: translateX(-50%);
  background: #2563eb;
  color: white;
  padding: 0.75rem 1.5rem;
  border-radius: 0 0 8px 8px;
  font-weight: 600;
  text-decoration: none;
  z-index: 10000;
  transition: top 0.2s ease;
}

.skip-link:focus {
  top: 0;
}

/* Le main doit avoir tabindex="-1" pour recevoir le focus */
#main {
  scroll-margin-top: 1rem;
}

Défi 2 : Bouton accessible

Créez un bouton avec focus-visible personnalisé, taille de toucher 44px, et respect de prefers-reduced-motion.

.btn {
  min-height: 44px;
  min-width: 44px;
  padding: 0.6rem 1.25rem;
  background: #2563eb;
  color: white;
  border: none;
  border-radius: 8px;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.2s, transform 0.2s;
}

.btn:focus-visible {
  outline: 3px solid #2563eb;
  outline-offset: 2px;
}

.btn:hover { background: #1d4ed8; }
.btn:active { transform: scale(0.98); }

@media (prefers-reduced-motion: reduce) {
  .btn { transition: none; }
}

Défi 3 : Thème dark accessible

Créez un thème dark mode utilisant prefers-color-scheme avec des contrastes vérifiés (≥ 7:1 pour le texte).

:root {
  --bg: #ffffff;
  --text: #1e293b;        /* 14.5:1 AAA */
  --text-sec: #475569;    /* 7:1 AAA */
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0f172a;
    --text: #f1f5f9;       /* 15.3:1 AAA */
    --text-sec: #cbd5e1;   /* 9.2:1 AAA */
  }
}

body {
  background: var(--bg);
  color: var(--text);
}

Défi 4 : Texte sr-only

Ajoutez une icône 🔍 dans un bouton de recherche, avec un texte sr-only lisible par les lecteurs d'écran.

<button class="search-btn">
  <span aria-hidden="true">🔍</span>
  <span class="sr-only">Rechercher</span>
</button>

.search-btn {
  min-height: 44px;
  min-width: 44px;
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 1rem;
  background: #2563eb;
  color: white;
  border: none;
  border-radius: 8px;
  font-weight: 600;
  cursor: pointer;
}

Défi 5 : Modal accessible

Créez une modal avec focus trap, aria-modal, et bouton fermer accessible.

/* CSS de la modal */
.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
}

.modal-content {
  background: white;
  border-radius: 12px;
  padding: 2rem;
  max-width: 500px;
  width: 90%;
  position: relative;
}

.modal-close {
  position: absolute;
  top: 1rem;
  right: 1rem;
  width: 44px;
  height: 44px;
  border: none;
  background: #f1f5f9;
  border-radius: 8px;
  font-size: 1.2rem;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}

.modal-close:focus-visible {
  outline: 3px solid #2563eb;
  outline-offset: 2px;
}

8. Projets pratiques

Projet 1 : Page d'accueil accessible

Créez une page d'accueil complète avec :

  • Skip link fonctionnel
  • Navigation avec aria-label et focus-visible
  • Contraste vérifié (tous les textes ≥ 4.5:1)
  • Thème dark mode avec prefers-color-scheme
  • prefers-reduced-motion pour toutes les animations
  • Boutons et liens ≥ 44×44px
  • Texte sr-only pour les icônes

Projet 2 : Formulaire accessible

Créez un formulaire de contact avec :

  • Labels associés (for/id)
  • Messages d'erreur avec role="alert"
  • Indices avec aria-describedby
  • Focus visible sur chaque champ
  • Validation en temps réel accessible
  • Contraste des messages d'erreur ≥ 4.5:1

Projet 3 : Dashboard accessible

Créez un mini-dashboard avec :

  • Sidebar avec navigation clavier
  • Graphiques avec alternatives textuelles (sr-only)
  • Tableaux avec en-têtes th et scope
  • Indicateurs avec aria-label
  • Mode forcé (forced-colors) supporté
  • Thème contraste élevé avec prefers-contrast

9. Révision

Récapitulatif des concepts clés

Concept Propriété / Méthode Valeurs clés
Contraste texte Ratio de contraste WCAG AA ≥ 4.5:1, AAA ≥ 7:1
Contraste large Ratio de contraste WCAG AA ≥ 3:1, AAA ≥ 4.5:1
Focus clavier :focus-visible outline, box-shadow
Focus clic :focus:not(:focus-visible) outline: none
Skip link .skip-link:focus top: 0
Toucher min-width/min-height 44px
Texte sr-only .sr-only clip, 1px×1px
Réduire mouvement prefers-reduced-motion reduceanimation: none
Schéma couleur prefers-color-scheme light, dark
Contraste élevé prefers-contrast more, less
Transparence prefers-reduced-transparency reduce
Mode forcé forced-colors active
Couleurs inversées inverted-colors filter: invert(1)
ARIA masquer aria-hidden="true" Masque des lecteurs d'écran
ARIA étiquette aria-label Label pour lecteur d'écran

Quiz rapide

  1. Quel est le ratio de contraste minimum pour un texte normal au niveau AA ?
  2. Quelle pseudo-classe CSS permet de cibler uniquement le focus clavier ?
  3. Quelle est la taille minimale recommandée pour les cibles de toucher ?
  4. Comment masquer visuellement un texte tout en le gardant accessible aux lecteurs d'écran ?
  5. Quelle media query permet de détecter la préférence de mouvement réduit ?
  6. Pourquoi ne faut-il jamais utiliser :focus { outline: none } sans alternative ?
  7. Quelle attribute ARIA masque un élément des lecteurs d'écran ?
  8. Quel est le bon ratio de contraste pour du texte sur fond sombre (dark mode) ?
  9. Comment créer un lien d'évitement (skip link) ?
  10. Qu'est-ce que forced-colors et quand est-il actif ?
Réponses :
  1. 4.5:1 pour le texte normal, 3:1 pour le texte large (≥ 18pt ou ≥ 14pt gras).
  2. :focus-visible — elle ne s'active qu'au clavier, pas au clic souris.
  3. 44×44 pixels (WCAG 2.2).
  4. La classe .sr-only avec clip: rect(0,0,0,0), width: 1px, height: 1px.
  5. @media (prefers-reduced-motion: reduce)
  6. Car cela rend le focus invisible pour les utilisateurs qui naviguent au clavier, rendant le site inutilisable.
  7. aria-hidden="true"
  8. Idéalement ≥ 7:1 (AAA) — ex : #f1f5f9 sur #0f172a = 15.3:1.
  9. Un lien avec position: absolute; top: -100% qui passe à top: 0 au :focus.
  10. forced-colors est actif en mode contraste élevé du système (surtout Windows). Il remplace les couleurs par la palette système.

Cheat sheet rapide

/* === FOCUS === */
:focus-visible { outline: 3px solid #2563eb; outline-offset: 2px; }
:focus:not(:focus-visible) { outline: none; }

/* === SKIP LINK === */
.skip-link { position: absolute; top: -100%; left: 50%; transform: translateX(-50%); }
.skip-link:focus { top: 0; }

/* === SR-ONLY === */
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border-width: 0; }

/* === TOUCHER === */
.touch-target { min-width: 44px; min-height: 44px; }

/* === PRÉFÉRENCES === */
@media (prefers-reduced-motion: reduce) { * { animation: none !important; } }
@media (prefers-color-scheme: dark) { :root { --bg: #0f172a; } }
@media (prefers-contrast: more) { :root { --border: #000; } }
@media (forced-colors: active) { .btn { border: 2px solid ButtonText; } }

/* === CONTRASTES === */
/* Texte normal AA: 4.5:1 | AAA: 7:1 */
/* Texte large AA: 3:1   | AAA: 4.5:1 */
/* UI components: 3:1 minimum */