Accessibilité Web en Pratique : Rôles ARIA et Guide de Conformité WCAG 2.2

前端工程

Les Quatre Principes de la WCAG 2.2

La WCAG 2.2 organise les exigences de conformité autour des principes POUR :

Principe Signification Exigence Centrale Exemple
Perceptible L'information est présentable Contenu perceptible par tous les utilisateurs Alt d'image, sous-titres, contraste
Utilisable L'interface est utilisable Utilisable via plusieurs méthodes de saisie Navigation clavier, temps suffisant, sans clignotement
Compréhensible Le contenu est compréhensible Le contenu et le fonctionnement sont clairs Navigation cohérente, indications d'erreur, aide à la saisie
Robuste Le contenu est compatible Analysable par les technologies d'assistance HTML valide, sémantique ARIA

Système de Rôles ARIA

Rôles de Structure du Document

<header role="banner">
  <nav role="navigation" aria-label="Main navigation">
    <ul role="menubar">
      <li role="menuitem"><a href="/home">Home</a></li>
      <li role="menuitem" aria-haspopup="true">
        Products
        <ul role="menu">
          <li role="menuitem"><a href="/saas">SaaS</a></li>
          <li role="menuitem"><a href="/api">API</a></li>
        </ul>
      </li>
    </ul>
  </nav>
</header>
<main role="main">
  <article role="article">
    <h2>Article Title</h2>
    <p>Content body</p>
  </article>
</main>
<footer role="contentinfo">Copyright info</footer>

Rôles de Widget

Rôle Objectif Propriétés Requises Interaction Clé
button Déclencheur cliquable Aucune Entrée/Espace active
checkbox Coché/décoché aria-checked Espace bascule
radio Option unique aria-checked Flèches basculent
slider Valeur de plage aria-valuenow, aria-valuemin, aria-valuemax Flèches ajustent
tablist / tab Panneaux d'onglets aria-selected Flèches basculent
dialog Modal/non-modal aria-modal Échap ferme
combobox Saisie sélectionnable aria-expanded Flèche sélectionner/filtre de saisie
tree / treeitem Structure arborescente aria-expanded Flèche développer/réduire

Propriétés et États ARIA

Référence Rapide des Propriétés Core

<!-- Labels and descriptions -->
<button aria-label="Close dialog">✕</button>
<input aria-labelledby="name-label" aria-describedby="name-hint">
<span id="name-label">Username</span>
<span id="name-hint">6-20 characters</span>

<!-- States -->
<div aria-expanded="true" aria-selected="false" aria-checked="mixed">
<div aria-hidden="true" aria-disabled="true" aria-busy="true">

<!-- Relationships -->
<div aria-owns="child-id" aria-controls="panel-id" aria-flowto="next-section">

<!-- Live regions -->
<div role="alert" aria-live="assertive">Error message</div>
<div aria-live="polite" aria-atomic="true" aria-relevant="additions text">Status update</div>

Première Règle : Ne Pas Abuser d'ARIA

Si un élément HTML natif fournit déjà la sémantique et l'interaction nécessaires, ne le réimplémentez pas avec ARIA.

<!-- ❌ Anti-pattern -->
<div role="button" tabindex="0" onclick="submit()">Submit</div>

<!-- ✅ Correct -->
<button type="submit">Submit</button>

Pratique : Modale Accessible

<div class="modal-overlay" id="modalOverlay" hidden>
  <div
    role="dialog"
    aria-modal="true"
    aria-labelledby="modalTitle"
    aria-describedby="modalDesc"
    class="modal"
  >
    <h2 id="modalTitle">Confirm Deletion</h2>
    <p id="modalDesc">This action cannot be undone. Are you sure you want to delete this record?</p>
    <div class="modal-actions">
      <button class="btn-cancel">Cancel</button>
      <button class="btn-confirm">Confirm Delete</button>
    </div>
  </div>
</div>
class AccessibleModal {
  constructor(dialogEl) {
    this.dialog = dialogEl;
    this.previousFocus = null;
  }

  open() {
    this.previousFocus = document.activeElement;
    this.dialog.hidden = false;
    const firstFocusable = this.dialog.querySelector(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    );
    firstFocusable?.focus();
    document.addEventListener('keydown', this.handleKeydown);
  }

  close() {
    this.dialog.hidden = true;
    this.previousFocus?.focus();
    document.removeEventListener('keydown', this.handleKeydown);
  }

  handleKeydown = (e) => {
    if (e.key === 'Escape') this.close();
    if (e.key === 'Tab') this.trapFocus(e);
  };

  trapFocus(e) {
    const focusable = [...this.dialog.querySelectorAll(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    )];
    const first = focusable[0];
    const last = focusable[focusable.length - 1];
    if (e.shiftKey && document.activeElement === first) {
      e.preventDefault();
      last.focus();
    } else if (!e.shiftKey && document.activeElement === last) {
      e.preventDefault();
      first.focus();
    }
  }
}

Pratique : Onglets Accessibles

<div class="tabs">
  <div role="tablist" aria-label="Account settings">
    <button
      role="tab"
      id="tab-profile"
      aria-selected="true"
      aria-controls="panel-profile"
      tabindex="0"
    >Profile</button>
    <button
      role="tab"
      id="tab-security"
      aria-selected="false"
      aria-controls="panel-security"
      tabindex="-1"
    >Security</button>
    <button
      role="tab"
      id="tab-notify"
      aria-selected="false"
      aria-controls="panel-notify"
      tabindex="-1"
    >Notifications</button>
  </div>

  <div role="tabpanel" id="panel-profile" aria-labelledby="tab-profile">
    <p>Profile settings content</p>
  </div>
  <div role="tabpanel" id="panel-security" aria-labelledby="tab-security" hidden>
    <p>Security settings content</p>
  </div>
  <div role="tabpanel" id="panel-notify" aria-labelledby="tab-notify" hidden>
    <p>Notification preferences content</p>
  </div>
</div>
document.querySelector('[role="tablist"]').addEventListener('keydown', (e) => {
  const tabs = [...e.currentTarget.querySelectorAll('[role="tab"]')];
  const idx = tabs.indexOf(document.activeElement);
  let nextIdx;

  if (e.key === 'ArrowRight') nextIdx = (idx + 1) % tabs.length;
  else if (e.key === 'ArrowLeft') nextIdx = (idx - 1 + tabs.length) % tabs.length;
  else if (e.key === 'Home') nextIdx = 0;
  else if (e.key === 'End') nextIdx = tabs.length - 1;
  else return;

  e.preventDefault();
  tabs.forEach((t) => { t.setAttribute('aria-selected', 'false'); t.tabIndex = -1; });
  tabs[nextIdx].setAttribute('aria-selected', 'true');
  tabs[nextIdx].tabIndex = 0;
  tabs[nextIdx].focus();
});

Pratique : Formulaire Accessible

<form aria-label="User registration">
  <div class="field">
    <label for="email">Email address <span aria-hidden="true">*</span></label>
    <input
      id="email"
      type="email"
      required
      aria-required="true"
      autocomplete="email"
      aria-invalid="false"
      aria-describedby="email-error"
    >
    <span id="email-error" role="alert" aria-live="assertive"></span>
  </div>

  <div class="field">
    <label for="password">Password</label>
    <input
      id="password"
      type="password"
      minlength="8"
      aria-describedby="pwd-rules pwd-strength"
    >
    <span id="pwd-rules">At least 8 characters, including uppercase, lowercase, and digits</span>
    <span id="pwd-strength" aria-live="polite"></span>
  </div>

  <fieldset>
    <legend>Notification method</legend>
    <label><input type="checkbox" name="notify" value="email"> Email</label>
    <label><input type="checkbox" name="notify" value="sms"> SMS</label>
  </fieldset>

  <button type="submit">Register</button>
</form>

Comparaison des Outils de Test Automatisés

Outil Type Couverture WCAG Intégration Caractéristique
axe-core Bibliothèque d'exécution A/AA CI + Navigateur La plus complète, zéro faux positif
Lighthouse Outil d'audit A CLI / DevTools Performance combinée + a11y
WAVE Extension navigateur A/AA Manuel Annotations visuelles
NVDA Lecteur d'écran Manuel Expérience réelle de l'utilisateur
Playwright a11y Test E2E A CI Intégré au flux de test
import { test } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test('accessibility check', async ({ page }) => {
  await page.goto('/register');
  const results = await new AxeBuilder({ page })
    .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
    .analyze();
  expect(results.violations).toEqual([]);
});

Nouveaux Critères de la WCAG 2.2

Nouveau Critère Niveau Description
2.4.11 Apparence du Focus AAA Zone de l'indicateur de focus ≥ 2px, contraste ≥ 3:1
2.4.12 Focus Non Masqué AAA Élément focalisé non masqué par un autre contenu
3.2.6 Aide Cohérente A Emplacement du mécanisme d'aide cohérent entre les pages
3.3.7 Saisie Redondante A Ne pas exiger la ressaisie d'informations déjà fournies
3.3.8 Authentification Accessible AA L'authentification ne repose pas sur des tests de fonction cognitive

Résumé des Meilleures Pratiques

  1. Éléments natifs d'abord : Utilisez <button>, <input> etc. au lieu de la réimplémentation avec ARIA
  2. Gestion du focus : Placez proactivement le focus à la position logique après les changements de modale/route
  3. Régions dynamiques avec précaution : Utilisez aria-live="assertive" uniquement pour les notifications critiques
  4. Automatisé + manuel : axe-core couvre ~30-40 % des problèmes ; les tests avec lecteur d'écran sont essentiels
  5. Intégration continue : Incluez les vérifications d'accessibilité dans le pipeline CI pour prévenir les régressions

Essayez ces outils exécutés localement dans le navigateur — aucune inscription requise →

#无障碍#ARIA#WCAG#屏幕阅读器#可访问性