Skip to Content

Chapitre 11.2 — Le problème SPA & le routing

⏱️ TL;DR — Dans une Single Page Application, cliquer sur un lien ne recharge pas la page : le routeur remplace le contenu en JavaScript. Côté visuel, c’est fluide. Côté lecteur d’écran, c’est muet : rien n’annonce qu’on a changé de page, et le focus reste bloqué sur le lien qu’on vient de cliquer — souvent en haut, hors du nouveau contenu. Deux choses à réparer : annoncer le changement (live region / route announcer) et déplacer le focus vers la nouvelle page. Next.js App Router fournit l’annonceur ; le focus et le title par page restent à ta charge.

🎯 Objectifs

  • Comprendre pourquoi une navigation client est silencieuse pour les technologies d’assistance.
  • Distinguer les deux correctifs : annonce (live region) et gestion du focus.
  • Savoir ce que Next.js App Router fait nativement, et ce qu’il te laisse à faire.
  • Mettre un title unique par page via metadata, et fixer lang sur <html>.

Ce qui se passe (et ne se passe pas) au changement de route

Un site multipage classique recharge complètement le document à chaque navigation. Ce rechargement est précieux pour l’a11y : le lecteur d’écran repart de zéro, lit le nouveau <title>, et le focus revient en haut du document. Tout ça, gratuitement.

Une SPA supprime ce rechargement. Le routeur (React Router, le routeur de Next.js…) intercepte le clic, va chercher les données, et remplace un morceau du DOM. Le navigateur, lui, ne considère pas qu’il y a eu « navigation » au sens classique. Conséquences pour un utilisateur de lecteur d’écran :

  • Aucune annonce : rien ne dit « nouvelle page ». L’utilisateur ne sait pas que le contenu a changé.
  • Focus bloqué : le focus reste sur le lien cliqué (ou nulle part si le lien a disparu du DOM). Il n’est pas ramené au début du nouveau contenu.
  • <title> figé : si tu ne le mets pas à jour, l’onglet et l’historique gardent l’ancien titre.

Côté utilisateur — Une personne aveugle clique sur « Mes cours ». Visuellement, la page a changé ; pour elle, rien n’a bougé : la synthèse vocale est restée silencieuse, et si elle appuie sur Tab, elle repart de l’endroit d’avant — parfois en plein milieu de l’ancien menu. Sans annonce ni focus déplacé, naviguer devient un jeu de devinettes.

Correctif 1 — Annoncer le changement de page

Le premier correctif consiste à annoncer la nouvelle page via une live region ARIA : un conteneur aria-live="polite" (ou role="status") dont le lecteur d’écran lit automatiquement le contenu quand il change. C’est le principe du route announcer. À chaque navigation, on y injecte le nom de la nouvelle page.

// Idée : une région polie que le routeur met à jour à chaque navigation <div aria-live="polite" role="status" className="sr-only"> {titrePageCourante} </div>

La classe sr-only masque visuellement la région tout en la laissant lisible par le lecteur d’écran (position absolue, 1px, overflow: hidden — voir Partie 6). L’utilisateur voyant ne voit rien ; l’utilisateur de lecteur d’écran entend « Mes cours ».

Correctif 2 — Déplacer le focus

Annoncer ne suffit pas : après la navigation, le focus doit arriver dans la nouvelle page. Sinon, l’utilisateur clavier reprend sa tabulation depuis l’ancien emplacement. La bonne cible : le <h1> de la nouvelle page (ou un conteneur principal). On lui met un tabIndex={-1} (focusable par programme, mais pas dans l’ordre du Tab) et on appelle .focus() après le rendu.

import { useRef, useEffect } from "react"; function Page({ children }) { const titreRef = useRef(null); useEffect(() => { titreRef.current?.focus(); }, []); // au montage de la nouvelle page return ( <main> <h1 tabIndex={-1} ref={titreRef}>Mes cours</h1> {children} </main> ); }

Le focus atterrit sur le titre : le lecteur d’écran lit « Mes cours, titre niveau 1 », et le Tab suivant repart de là. On détaille la mécanique du focus (timing, tabIndex={-1}, restitution) au Chapitre 11.3 et dans Gérer le focus.

Ce que Next.js App Router fait — et ne fait pas

Bonne nouvelle : Next.js intègre un annonceur de route. À chaque navigation client, il met à jour une live region interne qui annonce le titre de la nouvelle page au lecteur d’écran. Le Correctif 1 est donc largement couvert par le framework — à condition de lui donner de quoi annoncer, c’est-à-dire un title par page.

En App Router, ce titre vient de l’API metadata :

// app/mes-cours/page.tsx export const metadata = { title: "Mes cours — A11yLearn", }; export default function MesCours() { return <h1>Mes cours</h1>; }

Ce qui reste à ta charge, même avec Next.js :

  • Le focus. Next.js annonce, mais ne déplace pas systématiquement le focus vers le nouveau contenu. Le Correctif 2 (focus sur le <h1> ou un conteneur) reste ton travail.
  • Un title unique et pertinent par page, sinon l’annonceur répète le même titre partout.
  • La langue. Fixe lang sur la balise <html> de ton layout racine (app/layout.tsx), sinon la synthèse vocale peut lire ton français avec une prononciation anglaise.
// app/layout.tsx — la langue du document, indispensable export default function RootLayout({ children }) { return ( <html lang="fr"> <body>{children}</body> </html> ); }

⚠️ Piège — Croire que « Next.js gère l’accessibilité du routing », point final. Il fournit l’annonce, pas le focus. Une app Next.js sans gestion de focus laisse toujours l’utilisateur clavier bloqué sur l’ancien lien après chaque navigation. Vérifie-le toi-même : navigue au clavier, puis appuie sur Tab — si le focus repart du header au lieu du nouveau contenu, le correctif focus manque.

🧭 Sur A11yLearn — Sur A11yLearn (Next.js App Router), passer du tableau de bord à « Mes cours » était visuellement instantané mais silencieux au lecteur d’écran, et le focus restait sur l’onglet cliqué. On a d’abord donné un title distinct à chaque page via metadata : l’annonceur natif de Next.js s’est mis à énoncer « Mes cours — A11yLearn » à chaque navigation. Puis on a ajouté un focus programmatique sur le <h1> de chaque page. Résultat : la navigation est enfin annoncée et repositionnée, comme sur un site multipage — sans perdre la fluidité de la SPA.

✏️ Exercices

Exercice 1 — Diagnostic. Une app React Router : les liens marchent, l’URL change, le contenu change. Mais un testeur au lecteur d’écran dit « je ne sais jamais que la page a changé, et je dois re-tabuler depuis le début à chaque fois ». Nomme les deux problèmes et les deux correctifs.

✅ Solution

Problème 1 : aucune annonce — la navigation client ne recharge pas le document, donc rien n’est lu. Correctif : une live region (aria-live="polite" / route announcer) mise à jour avec le titre de la nouvelle page. Problème 2 : le focus n’est pas déplacé — il reste sur l’ancien lien. Correctif : après la navigation, déplacer le focus vers le <h1> (avec tabIndex={-1}) ou le conteneur principal de la nouvelle page.

Exercice 2 — Next.js, qui fait quoi ? Sur une app Next.js App Router, un dev affirme : « Pas besoin de m’occuper de l’a11y du routing, Next le fait. » Qu’est-ce qui est vrai, qu’est-ce qui manque ?

✅ Solution

Vrai : Next.js fournit un annonceur de route qui lit le title de la nouvelle page — à condition que chaque page ait un title distinct via metadata. Manque : la gestion du focus (Next ne le déplace pas vers le nouveau contenu), donc l’utilisateur clavier reste bloqué ; et il faut aussi penser à lang="fr" sur <html>. L’annonce ne dispense pas de gérer le focus.

🧠 Quiz de révision

1. Pourquoi une navigation SPA est-elle « muette » pour le lecteur d’écran ?

Parce que le routeur remplace le DOM en JavaScript sans recharger le document. Le navigateur ne signale pas de nouvelle navigation : le lecteur d’écran n’est pas relancé, ne lit pas de nouveau <title>, et rien n’annonce le changement de page. Sur un site multipage, le rechargement faisait ce travail gratuitement.

2. Quels sont les deux correctifs à appliquer ?

(1) Annoncer le changement via une live region (aria-live="polite" / route announcer) mise à jour avec le titre de la nouvelle page. (2) Déplacer le focus vers la nouvelle page (le <h1> avec tabIndex={-1}, ou le conteneur principal). L’annonce sans focus, ou l’inverse, laisse un problème.

3. Que fait Next.js App Router nativement pour le routing a11y ?

Il intègre un annonceur de route : à chaque navigation client, il met à jour une live region qui annonce le title de la nouvelle page — d’où l’importance d’un title distinct par page via metadata. Il ne gère pas le focus à ta place.

4. Où définir un title par page en App Router, et pourquoi ?

Via l’export metadata (export const metadata = { title: "…" }) dans chaque page.tsx. C’est ce titre que l’annonceur de route lit à la navigation ; sans titre distinct, l’annonceur répète le même intitulé partout et l’utilisateur ne se repère plus.

5. Pourquoi fixer lang="fr" sur <html> change-t-il l’expérience ?

Parce que le lecteur d’écran choisit sa prononciation selon la langue déclarée. Sans lang="fr" (ou avec un lang faux), un texte français peut être lu avec les règles phonétiques de l’anglais — quasi incompréhensible. En App Router, on le pose sur <html> dans app/layout.tsx.


Chapitre suivant : Gérer le focus en ReactuseRef, focus au montage, focus trap et restitution du focus.

Last updated on