Web Accessibility: WCAG 2.1 AA Compliance Guide

Build WCAG 2.1 AA compliant web apps with this practical guide. Covers semantic HTML, ARIA, keyboard navigation, color contrast, screen readers, and automated testing tools.

E
ECOSIRE Research and Development Team
|19 mars 202611 min de lecture2.4k Mots|

Fait partie de notre série Compliance & Regulation

Lire le guide complet

Accessibilité Web : Guide de conformité WCAG 2.1 AA

L'accessibilité n'est pas une fonctionnalité que vous ajoutez après le lancement : c'est un attribut de qualité fondamental, au même titre que les performances ou la sécurité. La conformité aux WCAG 2.1 AA est désormais légalement requise dans l’UE (Loi européenne sur l’accessibilité, entrée en vigueur en juin 2025), aux États-Unis (jurisprudence ADA Title III) et dans de nombreuses autres juridictions. Au-delà de la conformité, les interfaces accessibles convertissent mieux, sont mieux classées dans les recherches et servent environ 1,3 milliard de personnes handicapées dans le monde.

Ce guide est un manuel de mise en œuvre pratique et non une liste de contrôle. Vous apprendrez les quatre principes WCAG, les techniques les plus efficaces, comment tester systématiquement et comment intégrer l'accessibilité dans votre flux de travail de développement React/Next.js afin qu'il reste fixe.

Points clés à retenir

  • WCAG 2.1 AA requiert les quatre principes POUR : Perceptible, Opérable, Compréhensible, Robuste
  • Commencez par le HTML sémantique : il fournit 70 % d'accessibilité gratuitement avant l'ajout d'un ARIA.
  • Rapport de contraste des couleurs minimum : 4,5 : 1 pour le texte normal, 3 : 1 pour le texte de grande taille (18 pts/14 pts en gras)
  • Chaque élément interactif doit pouvoir être focalisé au clavier avec un indicateur de focus visible
  • Annonce des lecteurs d'écran basée sur l'arborescence d'accessibilité — test avec NVDA (Windows) et VoiceOver (Mac)
  • ARIA est un dernier recours : il change uniquement la manière dont les technologies d'assistance interprètent le DOM, pas le comportement.
  • Automatisez avec axe-core dans votre pipeline CI ; les tests manuels détectent ce qui manque à l'automatisation
  • Documentez votre déclaration d'accessibilité et fournissez un mécanisme de retour d'information permettant aux utilisateurs de signaler les problèmes

Les quatre principes POUR

Les WCAG 2.1 sont organisés autour de quatre principes. Chaque critère de réussite appartient à l’un d’entre eux.

Perceptible : les informations doivent être présentables de manière à ce que les utilisateurs puissent les percevoir. Cela couvre les alternatives textuelles aux images, les légendes des vidéos, un contraste de couleurs suffisant et le contenu qui ne repose pas uniquement sur la couleur pour transmettre un sens.

Utilisable : toutes les fonctionnalités doivent être utilisables via le clavier, avec suffisamment de temps pour interagir, sans contenu déclencheur de crise et une structure navigable (liens de saut, titres de page, ordre de mise au point).

Compréhensible : le contenu doit être lisible et prévisible. La langue doit être identifiée, les messages d'erreur doivent être descriptifs et les formulaires doivent avoir des étiquettes claires et des commentaires de validation.

Robuste : le contenu doit être interprétable par les technologies d'assistance actuelles et futures. Cela signifie un code HTML valide, une utilisation appropriée d'ARIA et des messages d'état annoncés sans nécessiter de focus.


HTML sémantique d'abord

Le HTML sémantique est l’investissement en matière d’accessibilité le plus efficace. Les éléments HTML natifs sont livrés avec des rôles d'accessibilité, des états et un comportement de clavier intégrés – aucun ARIA n'est requis.

// BAD: Generic divs with no semantics
<div class="button" onclick="submit()">Submit</div>
<div class="nav">
  <div class="link" onclick="navigate('/home')">Home</div>
</div>

// GOOD: Native semantics, free keyboard and screen reader support
<button type="submit" onClick={submit}>Submit</button>
<nav aria-label="Main navigation">
  <a href="/home">Home</a>
</nav>

Les régions emblématiques aident les utilisateurs de lecteurs d'écran à naviguer rapidement en sautant entre les sections :

// Every page should have these landmarks
<header>        {/* banner landmark */}
  <nav aria-label="Main">...</nav>
</header>
<main>           {/* main landmark */}
  <h1>Page Title</h1>
  <article>...</article>
  <aside aria-label="Related content">...</aside>
</main>
<footer>         {/* contentinfo landmark */}
  <nav aria-label="Footer">...</nav>
</footer>

La hiérarchie des titres doit être logique et ininterrompue :

// BAD: Skipped heading levels
<h1>Page Title</h1>
<h3>Section</h3>  {/* Skipped h2! */}

// GOOD: Sequential hierarchy
<h1>Page Title</h1>
<h2>Section</h2>
<h3>Subsection</h3>

Contraste des couleurs

Les WCAG 2.1 AA exigent :

  • Rapport de contraste de 4,5:1 pour le texte normal (inférieur à 18 pts/14 pts en gras)
  • Rapport de contraste 3:1 pour les gros textes (18 pt+ / 14 pt+ gras)
  • 3:1 pour les composants de l'interface utilisateur et les objets graphiques (boutons, icônes, bordures de saisie)
// Tailwind color contrast examples
// FAIL: gray-400 on white (#9ca3af on #fff = 2.8:1)
<p className="text-gray-400">This fails AA</p>

// PASS: gray-700 on white (#374151 on #fff = 10.7:1)
<p className="text-gray-700">This passes AA</p>

// For dark mode, test both themes separately
<p className="text-gray-700 dark:text-gray-300">
  gray-700 on white (10.7:1) / gray-300 on gray-900 (9.2:1)
</p>

Utilisez le WebAIM Contrast Checker ou l'outil de contraste du navigateur DevTools pendant le développement. Ajoutez ceci à votre système de Storybook ou de jetons de conception pour détecter les régressions :

// contrast-checker.ts
import { getContrast } from 'polished';

function assertContrast(fg: string, bg: string, level: 'AA' | 'AAA' = 'AA') {
  const ratio = getContrast(fg, bg);
  const required = level === 'AA' ? 4.5 : 7;
  if (ratio < required) {
    throw new Error(
      `Contrast ratio ${ratio.toFixed(2)}:1 fails WCAG ${level} (requires ${required}:1)`
    );
  }
}

Chaque élément interactif (liens, boutons, champs de formulaire, widgets personnalisés) doit être accessible et utilisable via le clavier.

Gestion de la concentration

// Skip link: first element on every page
// Allows keyboard users to jump past navigation
export function SkipLink() {
  return (
    <a
      href="#main-content"
      className="sr-only focus:not-sr-only focus:fixed focus:top-4 focus:left-4
                 focus:z-50 focus:px-4 focus:py-2 focus:bg-blue-600 focus:text-white
                 focus:rounded focus:ring-2 focus:ring-white"
    >
      Skip to main content
    </a>
  );
}

// Main content target
<main id="main-content" tabIndex={-1}>
  {/* tabIndex={-1} allows programmatic focus without appearing in tab order */}

Focus Trapping dans les modaux

Lorsqu'une boîte de dialogue s'ouvre, le focus doit être piégé à l'intérieur. Une fois fermé, le focus revient sur le déclencheur :

// focus-trap.tsx using @radix-ui/react-focus-trap (used internally by shadcn Dialog)
import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog';
import { useRef } from 'react';

export function AccessibleModal({ trigger, children, title }: Props) {
  const triggerRef = useRef<HTMLButtonElement>(null);

  return (
    <Dialog>
      <DialogTrigger ref={triggerRef} asChild>
        <button>Open</button>
      </DialogTrigger>
      <DialogContent
        // shadcn Dialog handles focus trap and returns focus to trigger on close
        aria-describedby="dialog-description"
      >
        <DialogTitle>{title}</DialogTitle>
        <p id="dialog-description" className="sr-only">
          {/* Screen reader description of dialog purpose */}
        </p>
        {children}
      </DialogContent>
    </Dialog>
  );
}

Indicateurs de mise au point visibles

WCAG 2.1 SC 2.4.11 (AA dans WCAG 2.2) nécessite un plan de mise au point minimum de 2 pixels. Ne supprimez jamais la mise au point sans remplacement :

/* globals.css */
/* NEVER do this: */
:focus { outline: none; }

/* DO this: custom, visible focus ring */
:focus-visible {
  outline: 2px solid hsl(var(--ring));
  outline-offset: 2px;
  border-radius: 4px;
}

/* Remove for mouse users (only show for keyboard) */
:focus:not(:focus-visible) {
  outline: none;
}

ARIA : quand et comment l'utiliser

Les attributs ARIA (Accessible Rich Internet Applications) modifient la manière dont les technologies d'assistance interprètent le DOM. La première règle d'ARIA : ne l'utilisez pas si un élément HTML natif existe pour votre cas d'utilisation.

###Étiquettes ARIA

// Icon-only button — screen reader has nothing to announce without aria-label
<button aria-label="Close dialog">
  <X className="h-4 w-4" aria-hidden="true" />
</button>

// Form field with visible label — use htmlFor, not aria-label
<label htmlFor="email">Email address</label>
<input id="email" type="email" />

// Input with visible description
<input
  id="password"
  type="password"
  aria-describedby="password-requirements"
/>
<p id="password-requirements">Must be at least 12 characters.</p>

Régions en direct ARIA

Annoncez des changements de contenu dynamiques sans déplacer le focus :

// Status messages (search results count, form submission status)
function SearchResults({ count, loading }: Props) {
  return (
    <>
      {/* aria-live="polite" waits for user to finish current action */}
      <div aria-live="polite" aria-atomic="true" className="sr-only">
        {loading ? 'Loading results...' : `${count} results found`}
      </div>
      {/* Visual result count (not for screen readers — aria-hidden) */}
      <span aria-hidden="true">{count} results</span>
    </>
  );
}

// Error messages (aria-live="assertive" interrupts immediately)
function FormError({ error }: { error: string | null }) {
  return (
    <div
      role="alert"
      aria-live="assertive"
      className={cn('text-red-500 text-sm', !error && 'hidden')}
    >
      {error}
    </div>
  );
}

ARIA pour les widgets personnalisés

Lorsque vous devez créer un widget personnalisé (panneau d'onglets, arborescence, liste déroulante), suivez exactement les modèles ARIA Authoring Practices Guide (APG) :

// Accessible tabs (ARIA tab pattern)
export function Tabs({ items }: { items: Tab[] }) {
  const [active, setActive] = useState(0);

  return (
    <div>
      <div role="tablist" aria-label="Content tabs">
        {items.map((item, i) => (
          <button
            key={item.id}
            role="tab"
            aria-selected={active === i}
            aria-controls={`panel-${item.id}`}
            id={`tab-${item.id}`}
            tabIndex={active === i ? 0 : -1} // Roving tabindex
            onClick={() => setActive(i)}
            onKeyDown={(e) => {
              if (e.key === 'ArrowRight') setActive((active + 1) % items.length);
              if (e.key === 'ArrowLeft') setActive((active - 1 + items.length) % items.length);
            }}
          >
            {item.label}
          </button>
        ))}
      </div>
      {items.map((item, i) => (
        <div
          key={item.id}
          role="tabpanel"
          id={`panel-${item.id}`}
          aria-labelledby={`tab-${item.id}`}
          hidden={active !== i}
        >
          {item.content}
        </div>
      ))}
    </div>
  );
}

Formulaires et gestion des erreurs

Les formulaires accessibles comptent parmi les améliorations les plus impactantes pour les utilisateurs souffrant de handicaps cognitifs et moteurs.

// Accessible form field with error state
function TextField({
  id,
  label,
  error,
  required,
  hint,
  ...props
}: TextFieldProps) {
  const hintId = hint ? `${id}-hint` : undefined;
  const errorId = error ? `${id}-error` : undefined;
  const describedBy = [hintId, errorId].filter(Boolean).join(' ') || undefined;

  return (
    <div>
      <label htmlFor={id} className="font-medium">
        {label}
        {required && <span aria-hidden="true" className="text-red-500 ml-1">*</span>}
        {required && <span className="sr-only">(required)</span>}
      </label>

      {hint && (
        <p id={hintId} className="text-sm text-gray-500 mt-1">
          {hint}
        </p>
      )}

      <input
        id={id}
        aria-required={required}
        aria-invalid={!!error}
        aria-describedby={describedBy}
        className={cn('input', error && 'border-red-500')}
        {...props}
      />

      {error && (
        <p id={errorId} className="text-sm text-red-500 mt-1" role="alert">
          {error}
        </p>
      )}
    </div>
  );
}

Images et médias

// Informative image
<img src="/chart.png" alt="Bar chart showing 40% revenue growth in Q4 2025" />

// Decorative image — empty alt hides it from screen readers
<img src="/divider.png" alt="" role="presentation" />

// Complex image — use aria-describedby for long descriptions
<figure>
  <img
    src="/architecture.png"
    alt="System architecture diagram"
    aria-describedby="arch-desc"
  />
  <figcaption id="arch-desc">
    The diagram shows three tiers: frontend Next.js on port 3000,
    NestJS API on port 3001, and PostgreSQL database on port 5433.
    Redis sits between the API and database layers.
  </figcaption>
</figure>

// SVG icons used as decoration
<svg aria-hidden="true" focusable="false">
  <use href="#icon-search" />
</svg>

Tests automatisés avec axe-core

pnpm add -D @axe-core/playwright axe-core
// tests/a11y/homepage.spec.ts
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test.describe('Homepage accessibility', () => {
  test('should have no WCAG 2.1 AA violations', async ({ page }) => {
    await page.goto('/');

    const results = await new AxeBuilder({ page })
      .withTags(['wcag2a', 'wcag2aa', 'wcag21aa'])
      .exclude('#third-party-widget') // Exclude known external violations
      .analyze();

    expect(results.violations).toEqual([]);
  });

  test('should be keyboard navigable', async ({ page }) => {
    await page.goto('/');
    // Tab through interactive elements and verify focus is visible
    await page.keyboard.press('Tab');
    const focusedElement = await page.evaluate(
      () => document.activeElement?.getAttribute('href')
    );
    expect(focusedElement).toBe('#main-content'); // Skip link
  });
});

Ajouter au pipeline CI :

# .github/workflows/ci.yml
- name: Run accessibility tests
  run: cd apps/web && npx playwright test tests/a11y --reporter=html
- uses: actions/upload-artifact@v4
  with:
    name: a11y-report
    path: apps/web/playwright-report/

Questions fréquemment posées

Quelle est la différence entre les WCAG 2.1 A, AA et AAA ?

Le niveau A est le minimum : un échec au niveau A signifie que le contenu est fondamentalement inaccessible à certains utilisateurs. Le niveau AA est la norme légale dans la plupart des juridictions et cible les besoins des utilisateurs les plus larges. Le niveau AAA est ambitieux : certains critères ne peuvent pas être remplis pour tous les types de contenu. Ciblez la conformité AA comme référence et visez l’AAA lorsque cela est possible.

L'utilisation d'une bibliothèque de composants telle que shadcn/ui rend-elle mon application accessible ?

shadcn/ui est construit sur les primitives de l'interface utilisateur Radix, qui sont accessibles de par leur conception : elles incluent les rôles ARIA corrects, la navigation au clavier et la gestion du focus. Cependant, vous devez toujours ajouter des étiquettes significatives, gérer les états d'erreur de manière accessible, garantir un contraste de couleurs suffisant avec votre thème personnalisé et tester avec de véritables technologies d'assistance. Les bibliothèques de composants réduisent la charge mais n'éliminent pas le besoin de tests d'accessibilité.

Comment tester avec un lecteur d'écran ?

Sous Windows, utilisez NVDA (gratuit) avec Firefox ou Chrome. Sur macOS, utilisez VoiceOver (intégré, Cmd+F5) avec Safari. Sur mobile, utilisez TalkBack (Android) ou VoiceOver (iOS). Testez les parcours des utilisateurs clés : remplissage de formulaires, interactions modales, navigation via des points de repère et lecture de contenu dynamique. Les tests des lecteurs d'écran détectent les annonces, l'ordre de lecture et le comportement de concentration qui manquent aux outils automatisés.

Qu'est-ce que le modèle tabindex itinérant ?

Le tabindex itinérant est le modèle de clavier pour les widgets composites (listes d'onglets, barres d'outils, groupes radio, arborescences). Un seul élément du groupe a tabIndex={0} à la fois : l'élément actif. Tous les autres obtiennent tabIndex={-1}. Les touches fléchées déplacent le focus au sein du groupe et mettent à jour quel élément a tabIndex 0. Cela empêche l'utilisateur de parcourir chaque élément du groupe - il entre dans le groupe avec Tab, navigue avec les touches fléchées et quitte avec Tab.

Comment gérer l'accessibilité du contenu dynamique chargé via AJAX ?

Utilisez les régions aria-live pour les mises à jour de statut (compte des résultats de recherche, sauvegarde des confirmations). Pour le remplacement de sections de page complète, déplacez le focus sur le titre ou le conteneur du nouveau contenu après le chargement. Pour les états de chargement, utilisez aria-busy="true" sur la région en cours de mise à jour et une région aria-live="polite" pour annoncer la fin. Testez toujours avec un lecteur d’écran pour vérifier que les annonces sont claires et opportunes.


Prochaines étapes

L'accessibilité du Web est une pratique continue et non un audit ponctuel. Commencez par corriger votre HTML sémantique et votre contraste de couleurs, puis superposez la navigation au clavier et ARIA pour les widgets complexes, et automatisez la validation WCAG dans votre pipeline CI pour détecter les régressions.

ECOSIRE construit des applications Web conformes aux WCAG 2.1 AA comme norme de base pour chaque projet. Si vous avez besoin d'un audit d'accessibilité ou si vous souhaitez créer une conformité à partir de zéro, découvrez nos services d'ingénierie front-end.

E

Rédigé par

ECOSIRE Research and Development Team

Création de produits numériques de niveau entreprise chez ECOSIRE. Partage d'analyses sur les intégrations Odoo, l'automatisation e-commerce et les solutions d'entreprise propulsées par l'IA.

Discutez sur WhatsApp