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.
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)
| 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 |
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
: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;
}
/* 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)
/* 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
/* 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;
}
/* 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 */
}
/* 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 */
}
}
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.
Détecte si l'utilisateur souhaite réduire les animations. Désactivez les animations décoratives et réduisez les transitions.
Détecte si l'utilisateur souhaite un contraste élevé. Augmentez les contrastes de texte et de bordures.
Détecte si l'utilisateur souhaite réduire la transparence. Remplacez les fonds translucides par des fonds opaques.
Mode contraste élevé du système (Windows HC). Utilisez forced-color-adjust pour préserver les couleurs essentielles.
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.
.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;
}
/* 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 avec aria-label — le lecteur d'écran lit "Supprimer l'élément"
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
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
aria-valuenow="65" — le lecteur d'écran lit "65%"
Contenu de l'onglet Général — focus clavier avec Tab + flèches.
/* 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; }
}
/* 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);
}
/* 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;
}
/* 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;
}
/* 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; }
}
/* 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> */
/* 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;
}
/* 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 */
}
/* 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;
}
}
/* 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 */
}
/* 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>
/* 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;
}
/* 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;
}
/* 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;
}
}
:focus-visible pour les outline personnalisés.<button> plutôt que <div onclick>./* 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) */
/* === 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;
}
}
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;
}
Projet 1 : Page d'accueil accessible
Créez une page d'accueil complète avec :
Projet 2 : Formulaire accessible
Créez un formulaire de contact avec :
Projet 3 : Dashboard accessible
Créez un mini-dashboard avec :
| 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 |
reduce → animation: 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 |
:focus { outline: none } sans alternative ?forced-colors et quand est-il actif ?:focus-visible — elle ne s'active qu'au clavier, pas au clic souris..sr-only avec clip: rect(0,0,0,0), width: 1px, height: 1px.@media (prefers-reduced-motion: reduce)aria-hidden="true"#f1f5f9 sur #0f172a = 15.3:1.position: absolute; top: -100% qui passe à top: 0 au :focus.forced-colors est actif en mode contraste élevé du système (surtout Windows). Il remplace les couleurs par la palette système./* === 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 */