Accessibilité · Démo interactive

Le web pour tous
sans effort surhumain

L'accessibilité n'est pas une couche en plus : c'est du HTML bien posé et quelques outils natifs. Focus clavier, annonces vocales, contraste, mouvement, formulaires — six réflexes qui changent tout.

01

:focus-visible — focus clavier

Le bon compromis : afficher l'anneau de focus uniquement quand l'utilisateur navigue au clavier, pas au clic souris. :focus-visible le fait nativement. Clique un bouton, puis navigue avec Tab pour voir la différence.

<button class="btn">Bouton A</button>
<button class="btn">Bouton B</button>
<button class="btn">Bouton C</button>
.btn {
  padding: .5rem 1rem; color: #c9d4e8;
  background: #1a2333; border: 1px solid #1e2d44; border-radius: 6px;
}
/* anneau de focus SEULEMENT au clavier */
.btn:focus-visible { outline: 3px solid #c9952a; outline-offset: 2px; }
/* jamais au clic souris */
.btn:focus:not(:focus-visible) { outline: none; }
02

ARIA & aria-live

Une région live annonce ses changements au lecteur d'écran, sans déplacer le focus. Indispensable pour les notifications, paniers, chargements. Clique et le message est vocalisé (aria-live="polite").

<button id="add">Ajouter au panier</button>
<div id="live" role="status" aria-live="polite"></div>
let count = 0;

document.getElementById('add').addEventListener('click', () => {
  count += 1;
  // le lecteur d'écran annonce ce nouveau texte, sans voler le focus
  document.getElementById('live').textContent =
    `Article ajouté — ${count} article${count > 1 ? 's' : ''} dans le panier`;
});
03

Contraste WCAG — vérificateur

Le contraste texte/fond doit atteindre 4.5:1 (texte normal) ou 3:1 (grand texte) pour le niveau AA. Choisis deux couleurs et vérifie le ratio en direct — la formule officielle de luminance relative.

Texte d'exemple — Aa
<div class="preview" id="preview">Texte d'exemple — Aa</div>
<label>Texte <input type="color" id="fg" value="#8492ac"></label>
<label>Fond <input type="color" id="bg" value="#0d1117"></label>
<div id="result" role="status" aria-live="polite"></div>
.preview { padding: 1.5rem; border-radius: 8px; text-align: center; font-size: 1.2rem; }
label { display: inline-flex; gap: .4rem; margin-right: 1rem; color: #c9d4e8; }
#result { margin-top: .75rem; font-family: monospace; font-size: .85rem; color: #c9d4e8; }
const fg = document.getElementById('fg');
const bg = document.getElementById('bg');
const preview = document.getElementById('preview');
const result = document.getElementById('result');

// luminance relative (formule officielle WCAG)
const chan = (v) => { v /= 255; return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4; };
function luminance(hex) {
  const h = hex.replace('#', '');
  const [r, g, b] = [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16));
  return 0.2126 * chan(r) + 0.7152 * chan(g) + 0.0722 * chan(b);
}
function update() {
  preview.style.color = fg.value;
  preview.style.background = bg.value;
  const l1 = luminance(fg.value), l2 = luminance(bg.value);
  const ratio = (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
  let verdict = 'échec — contraste insuffisant';
  if (ratio >= 7) verdict = 'AAA ✓';
  else if (ratio >= 4.5) verdict = 'AA ✓ texte normal';
  else if (ratio >= 3) verdict = 'AA grand texte seulement';
  result.textContent = `Ratio ${ratio.toFixed(2)}:1 — ${verdict}`;
}
fg.addEventListener('input', update);
bg.addEventListener('input', update);
update();
04

prefers-reduced-motion

Certaines personnes sont gênées, voire malades, face au mouvement. @media (prefers-reduced-motion: reduce) détecte leur réglage système et coupe les animations. L'anneau ci-dessous s'arrête si tu as activé cette préférence.

<div class="spinner"></div>
<p id="status" role="status" aria-live="polite"></p>
.spinner {
  width: 40px; height: 40px; border-radius: 50%;
  border: 4px solid #1e2d44; border-top-color: #c9952a;
  animation: spin 1s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* respecte le réglage système → coupe l'animation */
@media (prefers-reduced-motion: reduce) {
  .spinner { animation: none; }
}
const mq = window.matchMedia('(prefers-reduced-motion: reduce)');

const show = () => {
  document.getElementById('status').textContent = mq.matches
    ? 'Réduction du mouvement demandée → animation coupée.'
    : 'Aucune réduction demandée → animation active.';
};
show();
mq.addEventListener('change', show);
05

Formulaires accessibles

Un <label> relié, un message d'erreur annoncé via aria-describedby + role="alert", et aria-invalid : le lecteur d'écran sait quoi corriger. Valide avec un e-mail incorrect pour voir.

<form id="form" novalidate>
  <label for="email">Adresse e-mail</label>
  <input type="email" id="email" required aria-describedby="err"
         placeholder="prenom@exemple.be">
  <p id="err" role="alert"></p>
  <button type="submit">Valider</button>
</form>
const form  = document.getElementById('form');
const email = document.getElementById('email');
const err   = document.getElementById('err');

form.addEventListener('submit', (e) => {
  e.preventDefault();
  const valid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value.trim());
  // aria-invalid + role="alert" → le lecteur d'écran annonce quoi corriger
  email.setAttribute('aria-invalid', String(!valid));
  err.textContent = valid
    ? 'Parfait, adresse valide ✓'
    : 'Merci d’entrer une adresse e-mail valide.';
  if (!valid) email.focus();
});