AccessibilitéNext.jsWCAGReactBonnes pratiques

Accessibilité web avec Next.js : rendez votre site conforme WCAG sans tout réécrire

Rendez votre site Next.js accessible : HTML sémantique, navigation clavier, ARIA, contrastes et focus. Guide pratique WCAG avec exemples TypeScript.

AMAlexis Mouchon5 min de lecture

15 % de la population mondiale vit avec une forme de handicap, et pourtant l'immense majorité des sites web restent partiellement ou totalement inutilisables au clavier ou au lecteur d'écran. L'accessibilité est souvent perçue comme une contrainte réglementaire ou un chantier titanesque — c'est faux dans les deux cas. Bien menée, elle améliore l'expérience de tous vos utilisateurs, votre SEO, et se met en place progressivement sans réécrire votre application.

Dans cet article, je passe en revue les fondamentaux de l'accessibilité appliqués à un projet Next.js : HTML sémantique, navigation clavier, gestion du focus, ARIA utilisé à bon escient, contrastes et outils de test. Avec du code concret, comme d'habitude.

Pourquoi l'accessibilité n'est plus optionnelle

Trois raisons de s'y mettre dès maintenant, au-delà de l'évidence éthique.

La loi. L'European Accessibility Act est entré en application en juin 2025 : les services numériques à destination des consommateurs européens (e-commerce, banque, transport...) doivent être accessibles. En France, le RGAA (Référentiel Général d'Amélioration de l'Accessibilité) s'impose déjà au secteur public et s'étend progressivement au privé. Les sanctions existent et les mises en demeure aussi.

Le SEO. Google ne lit pas votre site avec des yeux : comme un lecteur d'écran, il parcourt votre HTML. Une structure sémantique propre, des attributs alt renseignés, une hiérarchie de titres cohérente — tout ce qui aide un utilisateur de lecteur d'écran aide aussi le crawler. Accessibilité et référencement partagent les mêmes fondations.

L'expérience utilisateur globale. Les sous-titres profitent à ceux qui regardent une vidéo dans le train. Les contrastes élevés aident en plein soleil. La navigation clavier sert les power users. Concevoir accessible, c'est concevoir robuste.

Le HTML sémantique : 80 % du travail

La majorité des problèmes d'accessibilité viennent d'un HTML construit à coups de <div> et de <span>. Les lecteurs d'écran s'appuient sur la sémantique des balises pour annoncer la structure de la page ; si tout est une div, l'utilisateur navigue à l'aveugle.

Voici les erreurs les plus fréquentes que je rencontre en audit, et leur correction :

// ❌ Une div cliquable n'est ni focusable ni annoncée comme un bouton
<div className="btn" onClick={handleSubmit}>
  Envoyer
</div>

// ✅ Un bouton est focusable, activable au clavier (Entrée/Espace)
// et annoncé comme tel par les lecteurs d'écran
<button type="submit" className="btn">
  Envoyer
</button>

Même logique pour la structure globale d'une page. Dans un layout Next.js, utilisez les landmarks HTML5 :

// app/layout.tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="fr">
      <body>
        <header>
          <nav aria-label="Navigation principale">{/* ... */}</nav>
        </header>
        <main id="contenu-principal">{children}</main>
        <footer>{/* ... */}</footer>
      </body>
    </html>
  );
}

Trois points d'attention souvent oubliés : l'attribut lang="fr" sur <html> (sans lui, un lecteur d'écran peut lire votre contenu français avec une prononciation anglaise), un seul <h1> par page, et une hiérarchie de titres sans saut de niveau — on ne passe pas d'un h2 à un h4.

Les images : l'attribut alt intelligent

Le composant next/image exige l'attribut alt, c'est un bon garde-fou. Mais un alt mal rédigé ne vaut guère mieux qu'un alt absent :

import Image from "next/image";

// ❌ Redondant : le lecteur d'écran annonce déjà "image"
<Image src="/equipe.jpg" alt="image de l'équipe" width={800} height={600} />

// ✅ Descriptif et utile
<Image
  src="/equipe.jpg"
  alt="L'équipe de cinq personnes réunie autour d'une table de réunion"
  width={800}
  height={600}
/>

// ✅ Image purement décorative : alt vide pour que le
// lecteur d'écran l'ignore complètement
<Image src="/motif-decoratif.svg" alt="" width={200} height={50} />

La règle : décrivez la fonction de l'image dans son contexte, pas son apparence. Une loupe dans un bouton de recherche a pour alt « Rechercher », pas « icône de loupe ».

Navigation clavier et gestion du focus

Testez votre site cinq minutes sans souris : Tab pour avancer, Shift+Tab pour reculer, Entrée pour activer. Si vous êtes bloqué quelque part, ou si vous ne savez plus où vous êtes, vos utilisateurs au clavier le sont aussi.

Ne supprimez jamais l'outline sans le remplacer

Le classique outline: none posé globalement « parce que c'est moche » rend le site inutilisable au clavier. Avec Tailwind, la bonne approche utilise focus-visible, qui n'affiche l'anneau de focus que lors d'une navigation clavier — pas au clic souris :

<button
  className="rounded-lg bg-blue-600 px-4 py-2 text-white
    focus-visible:outline-2 focus-visible:outline-offset-2
    focus-visible:outline-blue-400"
>
  En savoir plus
</button>

Le lien d'évitement (skip link)

Un utilisateur au clavier ne devrait pas tabuler à travers 15 liens de menu pour atteindre le contenu. Le skip link, premier élément focusable de la page, règle le problème :

// components/SkipLink.tsx
export function SkipLink() {
  return (
    <a
      href="#contenu-principal"
      className="sr-only focus:not-sr-only focus:absolute focus:left-4
        focus:top-4 focus:z-50 focus:rounded-lg focus:bg-white
        focus:px-4 focus:py-2 focus:shadow-lg"
    >
      Aller au contenu principal
    </a>
  );
}

La classe sr-only de Tailwind masque le lien visuellement tout en le laissant accessible aux lecteurs d'écran ; focus:not-sr-only le fait apparaître dès qu'il reçoit le focus clavier. Invisible pour la plupart des visiteurs, précieux pour les autres.

Gérer le focus dans une modale

Le piège classique des applications React : une modale s'ouvre, mais le focus reste derrière, sur le bouton qui l'a déclenchée. L'utilisateur au clavier tabule dans une page qu'il ne voit plus. Une modale accessible doit capturer le focus à l'ouverture, le confiner à l'intérieur, se fermer avec Échap et restituer le focus à l'élément déclencheur.

Plutôt que de réimplémenter cette mécanique (c'est plus subtil qu'il n'y paraît), utilisez l'élément natif <dialog> qui gère tout cela nativement :

"use client";

import { useRef } from "react";

export function ConfirmDialog() {
  const dialogRef = useRef<HTMLDialogElement>(null);

  return (
    <>
      <button onClick={() => dialogRef.current?.showModal()}>
        Supprimer mon compte
      </button>

      <dialog
        ref={dialogRef}
        className="rounded-xl p-6 backdrop:bg-black/50"
        aria-labelledby="dialog-title"
      >
        <h2 id="dialog-title" className="text-lg font-semibold">
          Confirmer la suppression
        </h2>
        <p>Cette action est irréversible.</p>
        <div className="mt-4 flex gap-2">
          <button onClick={() => dialogRef.current?.close()}>Annuler</button>
          <button className="bg-red-600 text-white">Supprimer</button>
        </div>
      </dialog>
    </>
  );
}

showModal() confine le focus, active la fermeture par Échap et rend le reste de la page inerte. Des bibliothèques comme Radix UI ou Headless UI offrent la même garantie pour des composants plus complexes (menus, comboboxes, onglets).

ARIA : moins, c'est mieux

La première règle d'ARIA, énoncée par le W3C lui-même : ne pas utiliser ARIA quand une balise HTML native fait le travail. Un <button> vaut mieux qu'une <div role="button" tabindex="0"> avec des gestionnaires clavier réimplémentés à la main. ARIA mal utilisé rend un site moins accessible qu'ARIA absent.

Cela dit, quelques attributs sont réellement utiles au quotidien :

// aria-label : nommer un bouton dont le contenu visuel est une icône
<button aria-label="Fermer la fenêtre" onClick={onClose}>
  <XIcon aria-hidden="true" />
</button>

// aria-expanded : indiquer l'état d'un élément dépliable
<button aria-expanded={isOpen} aria-controls="menu-mobile">
  Menu
</button>

// aria-current : signaler la page active dans une navigation
<Link href="/blog" aria-current={pathname === "/blog" ? "page" : undefined}>
  Blog
</Link>

Pour les contenus dynamiques — un message de confirmation après soumission de formulaire, par exemple — les régions live permettent d'annoncer un changement sans déplacer le focus :

// Le lecteur d'écran annonce le message dès qu'il apparaît
<div role="status" aria-live="polite">
  {isSuccess && <p>Votre message a bien été envoyé.</p>}
</div>

Le conteneur aria-live doit exister dans le DOM avant que le message n'y soit injecté, sinon l'annonce ne se déclenche pas — une erreur très fréquente.

Contrastes et formulaires : les deux gisements de non-conformité

Contrastes de couleurs

Le WCAG niveau AA exige un ratio de contraste de 4,5:1 pour le texte normal et 3:1 pour le texte large. Le gris clair sur fond blanc si répandu dans les designs modernes (text-gray-400 sur fond blanc : ratio ~2,8:1) est non conforme. Vérifiez vos couleurs avec l'inspecteur de Chrome (le picker de couleurs affiche le ratio directement) et intégrez ces contraintes dans votre thème Tailwind plutôt que de corriger au cas par cas.

Autre règle : la couleur ne doit jamais être le seul vecteur d'information. Un champ en erreur signalé uniquement par une bordure rouge est invisible pour un daltonien — ajoutez une icône et un message texte.

Formulaires accessibles

Chaque champ doit avoir un label visible et programmatiquement associé, et les erreurs doivent être reliées au champ concerné :

<div>
  <label htmlFor="email" className="block font-medium">
    Adresse email
  </label>
  <input
    id="email"
    type="email"
    autoComplete="email"
    aria-describedby={error ? "email-error" : undefined}
    aria-invalid={!!error}
    className="mt-1 rounded-lg border px-3 py-2"
  />
  {error && (
    <p id="email-error" role="alert" className="mt-1 text-sm text-red-700">
      {error}
    </p>
  )}
</div>

Le placeholder ne remplace jamais un label : il disparaît à la saisie et son contraste est généralement insuffisant. Si vous utilisez React Hook Form et Zod pour vos formulaires, cette structure s'intègre naturellement — j'en parle en détail dans mon article sur React Hook Form et Zod dans Next.js.

Tester : outils automatiques et vérifications manuelles

Les outils automatiques détectent environ 30 à 40 % des problèmes d'accessibilité — c'est un excellent début, pas une garantie de conformité.

Pour un retour immédiat pendant le développement, eslint-plugin-jsx-a11y est inclus par défaut dans la configuration ESLint de Next.js : il signale les alt manquants, les gestionnaires de clic sur des éléments non interactifs, etc. Pour l'analyse en runtime, Lighthouse (onglet Accessibility) et l'extension axe DevTools couvrent les erreurs structurelles.

Vous pouvez aussi intégrer axe à vos tests automatisés :

// __tests__/accessibility.test.tsx
import { render } from "@testing-library/react";
import { axe, toHaveNoViolations } from "jest-axe";
import HomePage from "@/app/page";

expect.extend(toHaveNoViolations);

it("la page d'accueil ne présente aucune violation axe", async () => {
  const { container } = render(<HomePage />);
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});

Complétez toujours par deux vérifications manuelles : une traversée complète au clavier, et un test avec un vrai lecteur d'écran (VoiceOver est intégré à macOS — Cmd+F5 —, NVDA est gratuit sur Windows). Dix minutes suffisent pour détecter les blocages majeurs.

Notez enfin que l'accessibilité rejoint la performance : un site rapide est plus utilisable pour tout le monde, notamment sur les appareils modestes. Les deux chantiers se renforcent — voyez mon guide sur les Core Web Vitals avec Next.js et l'optimisation des images avec next/image.

Conclusion

L'accessibilité n'est pas un projet à part, c'est une exigence de qualité qui s'intègre au quotidien : du HTML sémantique plutôt que des div, un focus visible et jamais piégé, ARIA avec parcimonie, des contrastes vérifiés dès le design, des formulaires correctement labellisés, et des tests automatiques doublés de vérifications manuelles. Commencez par le lint et un audit Lighthouse, corrigez les erreurs structurelles, puis traitez les parcours critiques (navigation, formulaires de contact, tunnel d'achat) : vous couvrirez l'essentiel des situations réelles sans réécriture massive.

Au-delà de la conformité, c'est un investissement direct dans la qualité perçue de votre site — et dans son référencement.

Vous souhaitez faire auditer l'accessibilité de votre site ou lancer un projet web conçu accessible dès le départ ? N'hésitez pas à me contacter ou à m'écrire à contact@alexis-mouchon.fr pour en discuter.