Skip to Content

Chapitre 11.5 — Pièges Next.js

⏱️ TL;DR — Next.js ne rend pas ton appli inaccessible, mais certains réflexes courants créent des trous : <html> sans lang, title qui ne change pas d’une page à l’autre, alt oublié, contenu injecté via dangerouslySetInnerHTML, <div onClick> au lieu de <button>, contenu dynamique non annoncé. Ce chapitre les rassemble en une checklist Next.js a11y à passer avant chaque mise en ligne.

🎯 Objectifs

  • Repérer les pièges d’accessibilité récurrents en Next.js.
  • Corriger lang, title et métadonnées par page.
  • Éviter les faux éléments interactifs et le contenu non annoncé.
  • Disposer d’une checklist Next.js prête à l’emploi.

Piège 1 — lang manquant sur <html>

Sans lang, le lecteur d’écran ne sait pas dans quelle langue prononcer la page (voir langue & lisibilité). En App Router, c’est dans le layout racine :

// app/layout.tsx export default function RootLayout({ children }) { return ( <html lang="fr"> <body>{children}</body> </html> ) }

Piège 2 — Le title de page qui ne change pas

Chaque page doit avoir un titre unique et descriptif : c’est la première chose qu’annonce un lecteur d’écran au chargement, et c’est essentiel au changement de route en SPA (voir le problème SPA). En App Router, via l’API metadata :

// app/cours/[slug]/page.tsx export async function generateMetadata({ params }) { const cours = await getCours(params.slug) return { title: `${cours.titre} — A11yLearn` } }

Piège 3 — <div onClick> au lieu de <button>

Le piège le plus courant, et pas propre à Next : un élément cliquable qui n’est ni focusable ni annoncé.

// ❌ Ni focusable, ni annoncé « bouton », ni activable au clavier <div onClick={ouvrir}>Ouvrir le menu</div> // ✅ Rôle, focus et clavier natifs <button type="button" onClick={ouvrir}>Ouvrir le menu</button>

⚠️ Piègeeslint-plugin-jsx-a11y attrape une partie de ces cas (<div onClick> sans rôle/clavier), mais pas tous. Ne compte pas sur le seul linter : garde le réflexe « action → <button>, navigation → <a> » (3.4).

Piège 4 — dangerouslySetInnerHTML

Injecter du HTML brut (contenu d’un CMS, markdown rendu) court-circuite le JSX et peut introduire du HTML non sémantique ou inaccessible (titres sautés, images sans alt, <div> cliquables). Si tu dois l’utiliser, contrôle et nettoie la source, et vérifie le rendu au lecteur d’écran. Pour A11yLearn, le contenu des cours vient de Moodle : on s’assure que le HTML importé respecte les titres et les alt (voir Partie 12).

Piège 5 — Contenu dynamique non annoncé

Un filtre qui met à jour une liste, un panier qui change, un message de chargement : visuellement évidents, muets pour le lecteur d’écran si rien ne l’annonce. Solution : une live region (6.4) pour annoncer « 12 résultats », « ajouté », etc. En React, un composant annonceur réutilisable règle la plupart des cas.

Piège 6 — autoFocus et hydration

autoFocus peut déplacer le focus de façon surprenante (surtout au chargement) et interférer avec la restauration de scroll ; utilise plutôt un focus explicite et contextuel géré au bon moment (gérer le focus). Attention aussi aux différences serveur/client (hydration) qui peuvent réinitialiser un focus posé trop tôt.

La checklist Next.js a11y

[ ] <html lang="fr"> dans le layout racine [ ] title unique et descriptif par page (metadata) [ ] focus déplacé + page annoncée au changement de route [ ] tout élément cliquable est <button> ou <a> (jamais <div onClick>) [ ] next/image : alt pertinent ou alt="" si décoratif [ ] next/link pour la navigation interne (vrai <a>) [ ] contenu dynamique (filtres, toasts) annoncé via live region [ ] dangerouslySetInnerHTML : source contrôlée et vérifiée [ ] eslint-plugin-jsx-a11y actif en CI [ ] test clavier + lecteur d'écran sur les parcours clés

🧭 Sur A11yLearn — Avant chaque déploiement d’A11yLearn, on passe cette checklist : lang="fr", un title par cours, la modale Radix testée, les alt des images de leçon, la live region qui annonce « progression enregistrée ». Dix minutes qui évitent l’essentiel des régressions.

💡 Réflexe — Beaucoup de ces pièges se préviennent une fois pour toutes : lang dans le layout, eslint-plugin-jsx-a11y en CI (Partie 14), et un <Button>/<Link> maison dans ton design system. L’accessibilité qui tient, c’est celle qu’on encode dans l’outillage plutôt que de la refaire à la main à chaque page.

✏️ Exercices

Exercice 1 — Trouve les trois bugs. Repère les problèmes d’accessibilité dans ce composant Next.

export default function Carte({ cours }) { return ( <div onClick={() => router.push(`/cours/${cours.slug}`)}> <img src={cours.image} /> <h4>{cours.titre}</h4> </div> ) }

✅ Solution

(1) La <div onClick> navigue mais n’est ni focusable ni annoncée : utiliser next/link (<Link><a>). (2) L’<img> n’a pas de alt : ajouter un alt descriptif ou alt="". (3) Un <h4> isolé peut casser la hiérarchie des titres selon le contexte (vérifier qu’il suit un <h3>). Version corrigée : un <Link> enveloppant, un <Image> avec alt, et un niveau de titre cohérent avec la page.

Exercice 2 — Annoncer un filtre. Une liste de cours se met à jour quand on change un filtre, mais le lecteur d’écran ne dit rien. Comment corriger ?

✅ Solution

Ajouter une live region (role="status" / aria-live="polite") qui annonce le résultat après filtrage, par exemple « 8 cours trouvés ». Le focus ne doit pas être volé : la région annonce discrètement pendant que l’utilisateur reste sur le filtre. C’est le pattern de la Partie 6.4.

🧠 Quiz de révision

1. Où déclarer la langue de la page en App Router ?

Sur l’élément <html lang="fr"> du layout racine (app/layout.tsx). Sans lang, le lecteur d’écran ne sait pas comment prononcer le contenu.

2. Pourquoi le title par page compte-t-il particulièrement en SPA ?

Parce qu’au changement de route côté client, le title est un des rares repères annoncés (avec la gestion du focus) pour signaler qu’on a changé de page. Un title unique et descriptif par page, via l’API metadata, est donc essentiel.

3. Quel est le risque de dangerouslySetInnerHTML ?

Injecter du HTML brut non contrôlé peut introduire du contenu non sémantique ou inaccessible (titres incohérents, images sans alt, faux boutons) et court-circuiter le JSX. Il faut contrôler la source et vérifier le rendu.

4. Comment rendre <div onClick> accessible ?

En le remplaçant par l’élément natif adapté : <button> pour une action, <a>/<Link> pour une navigation. Ils apportent rôle, focus et clavier gratuitement. Reconstruire tout ça sur une <div> est fastidieux et fragile.

5. Quelle est la meilleure façon de prévenir durablement ces pièges ?

Les encoder dans l’outillage : lang dans le layout, eslint-plugin-jsx-a11y en CI, des composants <Button>/<Link> maison dans le design system, une checklist de déploiement. L’a11y qui tient est celle qu’on n’a pas à refaire à la main à chaque page.


Partie suivante : Moodle & plateformes de formation — l’accessibilité côté LMS et contenu pédagogique.

Last updated on