18 juin 2026 · Tommy Bordas

Rendre une application Angular accessible, les patterns qui comptent

angularaccessibilitea11ycdkspa

Une application Angular pose des défis d'accessibilité que les sites classiques ignorent : navigation sans rechargement, contenu qui change sans bruit, focus perdu à chaque transition. Voici les patterns concrets pour rendre une SPA Angular accessible, avec le CDK et du vrai code.

Le piège des SPA : la navigation silencieuse

Sur un site classique, changer de page recharge le document : le lecteur d'écran annonce la nouvelle page, le focus repart du début. Dans une Single Page Application, rien de tout cela. Angular remplace une partie du DOM, l'URL change, mais pour un utilisateur de lecteur d'écran, il ne s'est rien passé. Le focus reste coincé sur le lien cliqué, et aucune annonce ne signale l'arrivée sur une nouvelle vue.

C'est le problème d'accessibilité numéro un d'Angular, et il est invisible tant qu'on n'a pas testé au clavier ou au lecteur d'écran.

À retenir : dans une SPA, vous devez recréer manuellement ce que le navigateur faisait gratuitement : annoncer le changement de page et replacer le focus. Sans ça, un utilisateur de lecteur d'écran est perdu dès la première navigation.

Pattern 1 : annoncer les changements de route

Le CDK d'Angular fournit LiveAnnouncer, qui pousse un message dans une région aria-live sans déplacer le focus. On l'utilise pour annoncer chaque changement de vue.

import { Component, inject } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';
import { LiveAnnouncer } from '@angular/cdk/a11y';
import { Title } from '@angular/platform-browser';
import { filter } from 'rxjs';

@Component({ selector: 'app-root', /* ... */ })
export class AppComponent {
  private router = inject(Router);
  private announcer = inject(LiveAnnouncer);
  private title = inject(Title);

  constructor() {
    this.router.events.pipe(
      filter(e => e instanceof NavigationEnd)
    ).subscribe(() => {
      this.announcer.announce(`Page : ${this.title.getTitle()}`, 'polite');
    });
  }
}

Pattern 2 : remettre le focus au bon endroit

Annoncer ne suffit pas : il faut aussi déplacer le focus vers la nouvelle vue, sinon la touche Tab repart d'où elle était. La bonne cible est en général le <h1> ou le conteneur principal de la nouvelle page.

this.router.events.pipe(
  filter(e => e instanceof NavigationEnd)
).subscribe(() => {
  const main = document.querySelector('main h1') as HTMLElement | null;
  // tabindex=-1 rend l'élément focusable sans l'ajouter à l'ordre de tabulation
  main?.setAttribute('tabindex', '-1');
  main?.focus();
});

Pattern 3 : piéger le focus dans une modale

Quand une boîte de dialogue s'ouvre, le focus doit y rester tant qu'elle est ouverte, et revenir à son point de départ à la fermeture. Le CDK fournit cdkTrapFocus pour ça.

<div class="modal" cdkTrapFocus cdkTrapFocusAutoCapture role="dialog"
     aria-modal="true" aria-labelledby="titre-modale">
  <h2 id="titre-modale">Confirmer la suppression</h2>
  <button (click)="confirmer()">Confirmer</button>
  <button (click)="fermer()">Annuler</button>
</div>

Si vous utilisez @angular/material, le MatDialog gère déjà le piège de focus, le rôle dialog et le retour de focus. C'est une raison de plus de s'appuyer sur des composants éprouvés plutôt que de réinventer une modale.

Pattern 4 : des formulaires réactifs accessibles

Un formulaire Angular doit relier ses erreurs au champ concerné. Les attributs aria-invalid et aria-describedby font le lien pour le lecteur d'écran.

<label for="email">Adresse e-mail</label>
<input id="email" type="email" formControlName="email"
       [attr.aria-invalid]="email.invalid && email.touched"
       [attr.aria-describedby]="email.invalid ? 'email-erreur' : null">

@if (email.invalid && email.touched) {
  <p id="email-erreur" role="alert">Veuillez saisir un e-mail valide.</p>
}

Le role="alert" fait annoncer l'erreur dès son apparition, sans déplacer le focus.

Pattern 5 : suivre le focus clavier vs souris

Le CDK expose FocusMonitor, qui distingue un focus pris au clavier d'un focus pris à la souris. Pratique pour n'afficher l'anneau de focus qu'au clavier, sans le code maison fragile que l'on voyait avant :focus-visible.

Outil CDK Rôle
LiveAnnouncer Annoncer un message en aria-live
cdkTrapFocus Confiner le focus dans une zone (modale)
FocusMonitor Savoir si le focus vient du clavier ou de la souris
cdkAriaLive Région live déclarative dans le template

Et les signals et le zoneless ?

Bonne nouvelle : les signals et le mode zoneless ne changent rien à l'accessibilité. Le rendu reste du DOM standard. Le seul point d'attention est le même qu'avant : quand un contenu se met à jour dynamiquement (un résultat de recherche, un compteur, un message), il faut l'annoncer via une région aria-live ou LiveAnnouncer, sinon le changement passe inaperçu pour un lecteur d'écran. Sur ce sujet, voir aussi : Angular signals et zoneless, ce qui change vraiment.

La checklist accessibilité Angular

  • Changement de route annoncé via LiveAnnouncer
  • Focus replacé sur le h1 ou le main à chaque navigation
  • cdkTrapFocus (ou MatDialog) sur toutes les modales
  • Champs de formulaire avec label lié, aria-invalid et aria-describedby
  • Erreurs annoncées via role="alert"
  • Contenus dynamiques poussés dans une région aria-live
  • Test complet au clavier et au lecteur d'écran

Pour aller plus loin

Sur l'optimisation d'une application Angular, voir aussi : Comment j'ai optimisé les performances d'une application Angular de 40 %.

Vous voulez mettre votre application Angular en conformité d'accessibilité ? Discutons-en.