Addaw, anar a la pàgina d'inici Donar

Lliçó 7 de 10 · 20 min de lectura · Revisada el 26/10/2026

SPA (React, Vue, Angular): encaminament, focus, avisos i títols

Estàs llegint una lliçó oberta. Matricula't gratis per fer els tests i els exercicis, desar el teu progrés i aconseguir el certificat.

Una aplicació d'una sola pàgina (SPA) canvia el contingut sense recarregar el document. Això trenca les tres coses que un navegador feia sol en cada càrrega: posar el focus al principi, anunciar el títol de la pàgina nova i començar a llegir des de dalt. A més, els frameworks faciliten construir interfícies senceres amb div i gestors d'esdeveniments, i les seves abstraccions (components, plantilles, portals) hi afegeixen els seus propis paranys. Aquesta lliçó recull el que cal resoldre en qualsevol SPA i com es fa a React, Vue i Angular.

El que cal resoldre en cada canvi de ruta

  1. Títol: actualitzar document.title amb el títol de la vista (2.4.2).
  2. Focus: moure'l a l'inici de la vista nova, normalment a l'<h1> amb tabindex="-1" (2.4.3); així el lector anuncia el títol i el Tab següent comença en el contingut. Algunes guies prefereixen enfocar el contenidor <main> o un enllaç de salt; l'important és que sigui coherent i que la persona sàpiga que ha canviat de pàgina.
  3. Avís: opcionalment, escriure «Pàgina carregada: Els meus expedients» en una regió role="status" global (4.1.3).
  4. Scroll: tornar a dalt, perquè els encaminadors solen mantenir la posició anterior.
  5. Estat de càrrega: si la vista triga, mostrar un «Carregant…» a la regió d'estat i aria-busy="true" al contenidor fins que arribin les dades; en acabar, moure el focus.

La resta del que vam veure en els cursos anteriors (HTML semàntic, formularis, teclat, ARIA, components) i en la lliçó de JavaScript d'aquest curs s'aplica igual dins d'un component de framework; el framework no ho fa per tu ni t'ho impedeix.

Infografia · Procés

Què cal fer en cada canvi de ruta d'una SPA

Cinc passos per recuperar el que el navegador feia sol en carregar una pàgina

  1. Pas 1: Indicar l'espera«Carregant…» a la regió d'estat i aria-busy al contenidor
  2. Pas 2: Actualitzar el títoldocument.title amb el nom de la vista i del lloc (2.4.2)
  3. Pas 3: Tornar a daltScroll a l'inici: els encaminadors conserven la posició anterior
  4. Pas 4: Moure el focusA l'h1 amb tabindex="-1": s'anuncia i el Tab següent continua en el contingut
  5. Pas 5: Anunciar el canvi«Pàgina carregada: …» en una regió role="status" permanent (4.1.3)
ADDAW · Cursos d'accessibilitat
Llegeix la infografia com a text

Procés de cinc passos que una aplicació d'una sola pàgina ha d'executar en cada canvi de ruta. Pas 1, indicar l'espera: mostrar «Carregant…» a la regió d'estat i posar aria-busy al contenidor. Pas 2, actualitzar el títol: document.title amb el nom de la vista i del lloc, segons 2.4.2. Pas 3, tornar a dalt: fer scroll fins a l'inici, perquè els encaminadors conserven la posició anterior. Pas 4, moure el focus: a l'h1 amb tabindex -1, perquè s'anunciï i el Tab següent continuï en el contingut nou. Pas 5, anunciar el canvi: escriure «Pàgina carregada» i el nom de la vista en una regió role status permanent, segons 4.1.3.

React

React Router no gestiona el focus i, tret que facis servir les seves funcions de metadades, tampoc el títol. La solució habitual és un component que es munta en cada ruta o un efecte que escolta els canvis d'ubicació:

Exemple

import { useEffect, useRef } from 'react';
import { useLocation } from 'react-router-dom';

export function Pagina({ titulo, children }) {
  const h1 = useRef(null);
  const { pathname } = useLocation();
  useEffect(() => {
    document.title = `${titulo} - Seu electrònica`;
    window.scrollTo(0, 0);
    h1.current?.focus();
  }, [pathname, titulo]);
  return (
    <main id="contenido">
      <h1 ref={h1} tabIndex={-1}>{titulo}</h1>
      {children}
    </main>
  );
}

En la primera càrrega de l'aplicació no cal moure el focus, perquè el navegador ja comença pel principi del document: si ho vols respectar, omet l'efecte en el primer renderitzat i aplica'l només en els canvis de ruta posteriors.

Particularitats de React que afecten l'accessibilitat:

  • L'atribut és htmlFor en lloc de for i tabIndex en lloc de tabindex; els atributs aria-* i role s'escriuen igual que en HTML.
  • Els identificadors de components reutilitzables s'han de generar amb useId() per evitar id duplicats que trenquin label/for i aria-labelledby.
  • Els portals (createPortal) col·loquen els modals al final del body: el focus s'ha de gestionar explícitament, i la resta de l'aplicació s'ha de fer inert.
  • Les regions actives han d'estar muntades de manera permanent (en el layout), no renderitzar-se condicionalment amb el missatge a dins.
  • Biblioteques amb una bona base d'accessibilitat: React Aria (Adobe), Radix Primitives, Headless UI. Evita biblioteques de components que no documentin la compatibilitat amb el teclat.

Vue

Vue Router exposa guardes de navegació (router.afterEach) i el meta de cada ruta, que és un bon lloc per al títol:

router.afterEach((to) => {
  document.title = `${to.meta.titulo} - Seu electrònica`;
  nextTick(() => {
    const h1 = document.querySelector('main h1');
    h1?.setAttribute('tabindex', '-1');
    h1?.focus();
    anunciar(`Pàgina carregada: ${to.meta.titulo}`);
  });
});

Detalls propis de Vue: v-if elimina l'element del DOM (equival a display:none), mentre que v-show l'oculta amb CSS (també fora de l'arbre d'accessibilitat); cap dels dos no deixa contingut fantasma, però v-if destrueix i recrea components, cosa que pot fer perdre el focus. Les transicions de <Transition> han de respectar prefers-reduced-motion. Per als id únics, Vue 3.5 incorpora useId(); en versions anteriors, genera un identificador per instància. Biblioteques: Vuetify té una compatibilitat raonable; Headless UI i Reka UI (abans Radix Vue) ofereixen primitives accessibles.

Angular

Angular és el framework que incorpora més eines de sèrie a través del CDK (Component Dev Kit), mòdul @angular/cdk/a11y:

  • LiveAnnouncer: servei per escriure en una regió activa global sense crear-la a mà: this.announcer.announce('Pàgina carregada: Els meus expedients', 'polite').
  • FocusTrap i la directiva cdkTrapFocus: cicle del focus dins de diàlegs.
  • FocusMonitor: detecta si el focus ha arribat per teclat, per ratolí o per programa, útil per a indicadors de focus personalitzats.
  • ListKeyManager i FocusKeyManager: implementen el roving tabindex i la navegació amb fletxes per a llistes, menús i pestanyes.
  • InteractivityChecker: comprova si un element és enfocable o tabulable.
this.router.events
  .pipe(filter((e) => e instanceof NavigationEnd))
  .subscribe(() => {
    this.title.setTitle(`${this.rutaActual.titulo} - Seu electrònica`);
    const h1 = document.querySelector('main h1') as HTMLElement | null;
    h1?.setAttribute('tabindex', '-1');
    h1?.focus();
  });

El servei Title de @angular/platform-browser gestiona el títol i, des d'Angular 14, la propietat title en la definició de cada ruta l'actualitza automàticament. Angular Material fa servir el CDK internament i els seus components (diàlegs, menús, pestanyes) tenen una base d'accessibilitat sòlida, tot i que sempre cal verificar els noms accessibles que hi aportes tu.

Error freqüent

Confiar que «la biblioteca de components és accessible». Una biblioteca pot donar un diàleg amb un cicle de focus perfecte, però si tu no li passes un títol, el diàleg no té nom; si la teva pestanya conté només una icona, no té nom; si renderitzes el component dins d'un <a>, nies elements interactius. La biblioteca resol el patró; el contingut i la integració són responsabilitat de qui la fa servir.

Problemes transversals de les SPA

  • Botons fets amb div: els frameworks permeten posar onClick a qualsevol cosa, i els sistemes de disseny propis en abusen. La regla 1 d'ARIA continua vigent.
  • Formularis controlats: la validació en temps real tecla a tecla, tan fàcil d'implementar amb estat reactiu, és agressiva; valida en sortir del camp o en enviar.
  • Càrrega mandrosa i esquelets: els skeletons de càrrega són contingut buit per al lector; acompanya'ls d'aria-busy i d'un missatge d'estat.
  • Scroll infinit: sense paginació ni botó «Carrega'n més», el peu de pàgina pot quedar inabastable i el lector no arriba mai al final. Ofereix sempre un botó.
  • Renderitzat en servidor i hidratació: l'HTML inicial ja ha de ser correcte (landmarks, títols), perquè és el que rep el lector abans que el JavaScript prengui el control.

Punt clau

Un component de layout que, en cada canvi de ruta, actualitza el títol, enfoca l'<h1>, fa scroll cap amunt i anuncia la càrrega, més una regió d'estat permanent i id únics per instància: amb això una SPA recupera el que el navegador feia sol. Tota la resta és HTML, teclat i ARIA com en qualsevol web.

Resum

  • En cada canvi de ruta: document.title, focus a l'<h1> amb tabindex="-1", scroll cap amunt i, si escau, avís en una regió d'estat.
  • React: useEffect amb useLocation, useId per als id, portals amb gestió del focus, regions actives muntades sempre.
  • Vue: router.afterEach amb nextTick, meta.titulo, compte amb v-if i la pèrdua del focus.
  • Angular: CDK a11y (LiveAnnouncer, FocusTrap, FocusKeyManager) i la propietat title de les rutes.
  • La biblioteca resol el patró, però els noms, el contingut i la integració continuen sent responsabilitat teva.

T'ha servit aquesta lliçó?

Crea el teu compte gratis i segueix el curs amb tot:

  • El test d'aquesta lliçó (3 preguntes) per comprovar el que has après
  • Exercicis pràctics amb pàgines reals
  • El teu progrés desat: continues on ho vas deixar, des de qualsevol dispositiu
  • Constància del curs amb les hores i certificat gratuït del nivell 3 quan aprovis l'examen

Entres amb el teu compte de Google o només amb el teu correu electrònic. No cal contrasenya.