El Mòdul 6 va tancar amb una troballa que ens va deixar una espina clavada: el P1 de la Marta. En confirmar la seva matrícula, Cursalia mostrava un bonic missatge verd de "Matrícula feta amb èxit!"… que la Marta, amb NVDA, no sentia mai. El missatge apareixia a la pantalla sense recarregar la pàgina, i un lector de pantalla no "mira" la pantalla: només coneix allò que el navegador li comunica a través de l'arbre d'accessibilitat. Si insereixes contingut al DOM sense més, qui no ho veu, no se n'assabenta. En aquesta lliçó resolem aquest problema —i tota la seva família— amb dues eines: les regions actives (live region) (aria-live) per anunciar canvis, i la gestió del focus per portar l'usuari cap a on passa l'acció. Amb això fem el salt de les pàgines estàtiques al contingut que canvia sense recarregar, que és on viu la web moderna.
Contingut
- El problema: el DOM canvia i ningú no se n'assabenta
- Regions actives (
aria-live) en profunditat politedavantassertive: la cortesia de l'anunci- Rols implícits:
status,alertilog - Afinar l'anunci:
aria-atomiciaria-relevant - Aplicat a Cursalia: resultats de filtre, qüestionari, toasts i matrícula
- Gestió del focus en canvis dinàmics
- Contingut asíncron: "carregar més",
aria-busyi estats de càrrega - Patrons difícils: tooltips, disclosure diferit i drag-and-drop
- Errors comuns i consells
- Exercicis
- Conclusió
- El problema: el DOM canvia i ningú no se n'assabenta
Un usuari que veu la pantalla detecta qualsevol canvi visual amb un cop d'ull: apareix un toast, canvia el nombre de resultats, s'acoloreix de vermell un error. Però les tecnologies d'assistència només anuncien, per defecte, allò cap a on l'usuari navega o on té el focus. Un canvi que passa lluny del focus —en una altra zona de la pàgina— passa totalment inadvertit per a la Marta (NVDA) o per a la Sofía, que mira amb la lupa al 300% una altra part de la pantalla.
Hi ha dues maneres de comunicar un canvi dinàmic a les tecnologies d'assistència, i cal triar bé quina:
| Tècnica | Què fa | Quan usar-la |
|---|---|---|
| Regió activa | Anuncia el canvi sense moure el focus | L'usuari continua en la seva tasca; només cal informar-lo (resultats, toasts, estat) |
| Gestió del focus | Porta el focus al contingut nou | El canvi exigeix atenció o interacció immediata (obrir modal, saltar a un error) |
La regla mental: l'usuari necessita anar a algun lloc, o només necessita assabentar-se'n? Si només necessita assabentar-se'n, usa una regió activa. Si necessita actuar sobre el que és nou, mou el focus. Confondre totes dues és l'origen de la meitat dels problemes de contingut dinàmic.
- Regions actives (
aria-live) en profunditat
aria-live) en profunditatUna regió activa (live region) és un element del DOM marcat perquè, quan el seu contingut canviï, la tecnologia d'assistència l'anunciï automàticament encara que el focus estigui en un altre lloc. Als Mòduls 4 i 5 la vam esmentar de passada; aquí l'obrim en canal. Es declara amb l'atribut aria-live:
<!-- El contenidor existeix des del principi, buit. -->
<div id="anunci-filtre" aria-live="polite"></div>Quan JavaScript escrigui dins d'aquest div, NVDA el llegirà. El mecanisme és senzill però té una regla d'or que gairebé tothom incompleix:
La regió activa ha d'existir al DOM abans que canviï el seu contingut. El lector de pantalla "vigila" les regions que ja coneix. Si crees el
<div aria-live>i li poses text en el mateix instant, molts lectors no arriben a registrar-lo a temps i no anuncien res.
És a dir: primer pintes el contenidor buit (en carregar la pàgina o la vista), i després, en un segon moment, escrius a dins. Aquest matís —regió creada tard— és l'error número u amb aria-live, i el tornarem a veure a la secció 10.
polite davant assertive: la cortesia de l'anunci
polite davant assertive: la cortesia de l'anunciaria-live admet dos valors útils (off és el tercer i significa "no anunciar"):
| Valor | Comportament | Usa'l per a |
|---|---|---|
polite |
Espera que el lector acabi el que està dient, i aleshores anuncia | La immensa majoria de casos: resultats, estats, confirmacions |
assertive |
Interromp de seguida allò que el lector estigui dient | Només emergències: errors crítics, avisos que el temps s'esgota, pèrdua de dades |
assertive és com cridar en una conversa: de vegades necessari, gairebé sempre groller. Si n'abuses, interromps constantment la Marta i li impedeixes sentir el que estava llegint. Per defecte, polite. Reserva assertive per a allò que de debò no pot esperar.
<!-- Bé: un resultat de cerca no és una emergència -->
<div aria-live="polite" id="resultats-count"></div>
<!-- Bé: un error de sessió a punt d'expirar SÍ que interromp -->
<div aria-live="assertive" id="avis-sessio"></div>
<!-- Malament: interrompre la Marta cada vegada que canvia un comptador trivial -->
<div aria-live="assertive" id="comptador-visites"></div>
- Rols implícits:
status, alert i log
status, alert i logEn lloc d'aria-live "a mà", sovint és millor usar un rol que ja porta el comportament de regió activa incorporat. Són més semàntics i més robustos entre navegadors i lectors:
| Rol | Equival a | Ús típic a Cursalia |
|---|---|---|
role="status" |
aria-live="polite" + aria-atomic="true" |
"3 cursos trobats", "Desat", "S'està carregant…" |
role="alert" |
aria-live="assertive" + aria-atomic="true" |
Error de formulari de matrícula, sessió caducada |
role="log" |
aria-live="polite" (missatges nous al final) |
Xat del fòrum, registre d'activitat del tauler |
<!-- Comptador de resultats del catàleg: status (polite) -->
<p role="status" id="comptador-resultats"></p>
<!-- Error crític de matrícula: alert (assertive) -->
<p role="alert" id="error-matricula"></p>L'avantatge de role="status" i role="alert" és que ja porten aria-atomic="true" i una semàntica clara. role="log" és diferent: està pensat per a una seqüència de missatges que es van acumulant (el xat del fòrum de Cursalia), on només importa el que és nou i arriba al final, no rellegir tot l'historial.
- Afinar l'anunci:
aria-atomic i aria-relevant
aria-atomic i aria-relevantDos atributs permeten controlar quant i què anuncia la regió quan canvia:
aria-atomic: si éstrue, el lector anuncia tota la regió completa cada vegada que alguna cosa canvia a dins. Si ésfalse(per defecte aaria-live), anuncia només el node que va canviar.
<!-- aria-atomic="true": volem que es llegeixi la frase sencera i amb sentit -->
<p aria-live="polite" aria-atomic="true" id="estat-desat">
Progrés desat a les <span id="hora">14:32</span>
</p>Si en actualitzar només l'<span> de l'hora tinguéssim aria-atomic="false", la Marta sentiria un escarit "14:35" sense context. Amb aria-atomic="true" sent la frase completa: "Progrés desat a les 14:35". Per això status i alert l'activen per defecte: gairebé sempre vols el missatge amb el seu context.
aria-relevant: indica quins tipus de canvi disparen un anunci (additions,removals,text,all). El seu valor per defecte (additions text) serveix per a la majoria de casos: anuncia el que s'afegeix i els canvis de text, però no el que s'elimina. Rarament caldrà que el toquis; si un toast desapareix, normalment no vols anunciar la seva desaparició.
Consell: a la pràctica,
role="status"irole="alert"amb contenidors preexistents cobreixen el 90% de les teves necessitats. Reservaaria-atomic/aria-relevantper a casos concrets on l'anunci per defecte no digui el que vols.
- Aplicat a Cursalia: resultats de filtre, qüestionari, toasts i matrícula
Nombre de resultats en filtrar el catàleg
Quan la Marta aplica el filtre que vam construir al Mòdul 5, la llista de cursos s'actualitza sense recarregar. Un usuari que hi veu observa que la graella canvia; la Marta necessita sentir quants resultats hi ha:
<!-- Present des de la càrrega de la pàgina, buit -->
<p role="status" id="comptador-resultats"></p>function aplicarFiltre(cursos) {
pintarGraella(cursos);
// Escrivim DESPRÉS, en un contenidor que ja existia:
document.getElementById('comptador-resultats').textContent =
`${cursos.length} cursos trobats`;
}Ara, en filtrar, NVDA anuncia "12 cursos trobats" sense que la Marta perdi la seva posició a la pàgina.
Retroacció d'un qüestionari
En respondre una pregunta del qüestionari, la correcció ("Correcte" / "Incorrecte, la resposta era…") apareix dinàmicament. Com que és informació important però no una emergència, role="status" (polite) hi encaixa:
function corregir(esCorrecta, explicacio) {
const fb = document.getElementById('feedback-pregunta');
fb.textContent = esCorrecta
? 'Correcte. Ben fet!'
: `Incorrecte. ${explicacio}`;
}Notificacions / toasts
Els toasts són el cas clàssic de regió activa. No els moguis mai el focus (interrompria la tasca de l'usuari): només anuncia'ls. Un toast informatiu va en una regió polite; un d'error, en assertive:
<!-- Dues regions fixes, una per a cada urgència -->
<div role="status" id="toast-info"></div>
<div role="alert" id="toast-error"></div>function mostrarToast(missatge, tipus = 'info') {
const regio = tipus === 'error'
? document.getElementById('toast-error')
: document.getElementById('toast-info');
regio.textContent = missatge; // s'anuncia tot sol
// (la desaparició visual del toast al cap de 5 s no cal anunciar-la)
}El missatge d'èxit de matrícula: el P1 de la Marta, resolt
I aquí tanquem l'espina del Mòdul 6. El problema era que el missatge d'èxit s'inseria al DOM sense cap regió activa. La solució és exactament la mateixa tècnica:
<!-- Regió present a la pàgina del flux de matrícula, buida d'inici -->
<div role="status" id="resultat-matricula"></div>async function confirmarMatricula(cursId) {
await api.matricular(cursId);
// El missatge ara SÍ que s'anuncia a la Marta:
document.getElementById('resultat-matricula').textContent =
'Matrícula feta amb èxit! Ja tens accés al curs.';
}Amb tres línies, la Marta passa de no saber si la seva matrícula ha funcionat a sentir la confirmació. Aquest és el poder —i la senzillesa— d'una regió activa ben posada.
- Gestió del focus en canvis dinàmics
Les regions actives anuncien sense moure el focus. Però de vegades l'usuari necessita anar al contingut nou. La pregunta clau: on envio el focus quan insereixo o elimino contingut?
- En inserir contingut amb què cal interactuar (obrir un modal, expandir un formulari, mostrar un tauler d'errors per corregir): mou el focus al contingut nou. En un tauler d'errors de validació, porta el focus al resum d'errors o al primer camp no vàlid.
- En eliminar l'element que tenia el focus (esborrar una fila de "els meus cursos", tancar una targeta): si deixes que el focus caigui al
<body>, l'usuari de teclat es perd. Mou el focus a un lloc predictible i relacionat: l'element següent de la llista, el contenidor pare, o el botó que va originar l'acció.
// Esborrar un curs de "els meus cursos" i no perdre el focus
function eliminarCurs(fila) {
const seguent = fila.nextElementSibling || fila.previousElementSibling;
fila.remove();
// Enviar el focus a un lloc amb sentit, no deixar-lo mai orfe
(seguent ?? document.getElementById('llista-mis-cursos')).focus();
}Recorda del Mòdul 5 que, per poder enfocar per programa un contenidor que no és enfocable de manera nativa (un <div>, un <h2>, una <ul>), necessita tabindex="-1": enfocable per codi però fora de l'ordre de tabulació natural.
- Contingut asíncron: "carregar més",
aria-busy i estats de càrrega
aria-busy i estats de càrregaEl contingut que arriba per xarxa (fetch) planteja dos reptes: avisar que s'està carregant i gestionar el focus quan arriba.
Estats de càrrega amb aria-busy
Mentre una regió s'està actualitzant, aria-busy="true" li diu a la tecnologia d'assistència "això encara no està a punt, espera". En acabar, el poses a false:
async function carregarProgres() {
const panell = document.getElementById('panell-progres');
panell.setAttribute('aria-busy', 'true');
const dades = await api.progres();
pintarProgres(dades);
panell.setAttribute('aria-busy', 'false'); // ja està a punt
}Combina'l amb un missatge "S'està carregant…" en una regió status perquè l'usuari senti l'estat, no un silenci incòmode:
<div id="panell-progres" aria-busy="false">
<p role="status" id="estat-carrega"></p>
<!-- contingut -->
</div>El botó "Carregar més"
Quan el catàleg de Cursalia carrega més cursos en prémer "Carregar més", l'usuari de teclat té un problema subtil: prem el botó, s'afegeixen 12 cursos a sobre del botó… i ara on és el seu focus? Bones pràctiques:
- Anuncia quants elements nous s'han afegit (regió
status): "12 cursos més carregats". - Mou el focus al primer dels elements nous (amb
tabindex="-1"), perquè l'usuari continuï just on s'ha afegit el contingut, no de tornada al principi.
async function carregarMes() {
const nous = await api.mesCursos();
const primerNou = pintarIRetornarPrimer(nous); // retorna el node, amb tabindex="-1"
document.getElementById('estat-carrega').textContent =
`${nous.length} cursos més carregats`;
primerNou.focus(); // continuïtat per al teclat
}
- Patrons difícils: tooltips, disclosure diferit i drag-and-drop
Tooltips
Un tooltip accessible s'associa al control amb aria-describedby i s'ha de poder mostrar tant amb el focus com amb el hover (WCAG 1.4.13, Content on Hover or Focus: descartable amb Esc, apuntable i persistent). No posis mai informació essencial només en un tooltip: és contingut secundari per definició.
<button aria-describedby="tip-desar">Desar esborrany</button>
<div role="tooltip" id="tip-desar" hidden>
Es desa automàticament cada 30 segons.
</div>Amb aria-describedby, quan la Marta enfoca el botó, NVDA llegeix "Desar esborrany, botó" i tot seguit la descripció. Aquí no necessites aria-live: l'associació per describedby ja fa que es llegeixi en enfocar.
Disclosure de contingut diferit (lazy)
De vegades el contingut d'un disclosure (del Mòdul 5) no existeix fins que s'obre: es demana per xarxa en expandir. El repte és que la regió activa i el focus funcionin després que arribi el contingut, no abans:
async function obrirSeccioDiferida(btn, panell) {
btn.setAttribute('aria-expanded', 'true');
panell.hidden = false;
panell.setAttribute('aria-busy', 'true');
panell.innerHTML = `<p role="status">S'està carregant el contingut…</p>`;
const html = await api.seccio(panell.dataset.id);
panell.innerHTML = html; // ara sí que existeix el contingut
panell.setAttribute('aria-busy', 'false');
}Una nota sobre drag-and-drop accessible
Arrossegar i deixar anar amb ratolí és, per naturalesa, inaccessible per a qui usa només teclat (la Lucía) o un lector de pantalla (la Marta). WCAG 2.2 va afegir el criteri 2.5.7 (Dragging Movements, AA), que exigeix que tota funcionalitat d'arrossegament tingui una alternativa que no requereixi arrossegar. Per reordenar els cursos preferits de Cursalia, no n'hi ha prou amb el drag-and-drop: cal oferir botons "Pujar / Baixar" operables per teclat i anunciar el canvi de posició en una regió activa ("Curs mogut a la posició 2 de 5"). L'arrossegament és un extra visual, no l'única via. Això enllaça directament amb el que vam veure al Mòdul 5 sobre operar-ho tot per teclat.
Errors comuns i consells
- Crear la regió activa i omplir-la en el mateix instant. El lector no arriba a registrar-la i no anuncia res. Pinta el contenidor buit primer (en carregar la vista) i escriu-hi a dins després.
- Abusar d'
assertive. Interromp constantment. Per defectepolite/status;assertive/alertnomés per a emergències reals. - Inserir massa a la regió activa. Si hi aboques un bloc HTML enorme, el lector intenta llegir-lo sencer i satura. La regió ha de contenir un missatge breu, no mig catàleg.
- Anuncis que es trepitgen. Dues regions
assertivedisparant-se alhora, o actualitzar la mateixa regió tres vegades en 200 ms: l'usuari sent un batibull. Agrupa el missatge i escriu una sola vegada. - Moure el focus quan només calia anunciar (i viceversa). Un toast que roba el focus interromp la tasca; un modal que només s'"anuncia" però no rep el focus deixa el teclat fora. Tria segons la taula de la secció 1.
- Deixar el focus orfe en eliminar contingut. Si esborres l'element enfocat, reubica el focus en un veí amb sentit.
- Consell: tingues a la pàgina una sola regió
statusi una solaalertreutilitzables, presents des de l'arrencada. És més fiable que crear regions noves al vol.
Exercicis
Exercici 1. Aquest codi intenta anunciar el nombre de resultats del filtre, però la Marta no sent res. Identifica l'error i corregeix-lo.
function filtrar(cursos) {
const regio = document.createElement('div');
regio.setAttribute('aria-live', 'polite');
regio.textContent = `${cursos.length} resultats`;
document.body.appendChild(regio);
}Exercici 2. Classifica cada missatge de Cursalia com a role="status" (polite) o role="alert" (assertive) i justifica-ho: (a) "Progrés desat", (b) "La teva sessió expirarà d'aquí a 60 segons", (c) "3 cursos trobats", (d) "Error: no s'ha pogut processar el pagament".
Exercici 3. En prémer "Carregar més" al catàleg, s'afegeixen 12 cursos i el focus de la Lucía es queda al botó, que ara ha baixat molt a la pàgina. Descriu els dos ajustos d'accessibilitat que aplicaries i per què.
Solucions
Solució 1. L'error és que la regió es crea i s'emplena en el mateix moment (i s'afegeix al DOM ja amb text a dins). El lector de pantalla no la tenia registrada com a regió activa, així que no anuncia el canvi. La solució és tenir el contenidor aria-live/role="status" present i buit des d'abans, i només escriure el seu textContent en el moment del canvi:
function filtrar(cursos) {
document.getElementById('resultats').textContent = `${cursos.length} resultats`;
}Solució 2. (a) status: desar és una confirmació rutinària, no urgeix. (b) alert: la sessió a punt d'expirar és temps sensible i pot provocar pèrdua de dades; interrompre està justificat. (c) status: un comptador de resultats és informació passiva, no ha d'interrompre mai. (d) alert: un error de pagament bloqueja una tasca crítica i l'usuari ho ha de saber immediatament.
Solució 3. Primer, anunciar el canvi amb una regió role="status": "12 cursos més carregats", perquè la Marta sàpiga que l'acció ha tingut efecte. Segon, moure el focus al primer curs acabat d'afegir (donant-li tabindex="-1" per poder enfocar-lo per programa), de manera que la Lucía continuï navegant des del contingut nou en lloc de quedar-se en un botó que ara ha quedat despenjat. Així combinem anunci (per al lector) i focus (per al teclat), cadascun resolent la seva part.
Conclusió
Has après a fer accessible el contingut que canvia sense recarregar: regions actives per anunciar (polite/assertive, status/alert/log, aria-atomic/aria-relevant) i gestió del focus per dirigir l'usuari quan cal interactuar. I hem tancat per fi el P1 de la Marta del Mòdul 6: el seu missatge de matrícula ara se sent. La clau que t'has d'endur: pregunta't sempre si l'usuari necessita assabentar-se'n o necessita anar-hi, i tria l'eina adequada.
Però fins ara hem parlat de canvis dins d'una pàgina. Què passa quan tota l'aplicació és una SPA i ni tan sols la "navegació" recarrega la pàgina, sinó que reescriu el DOM sencer per enrutament de client? El lector de pantalla no anuncia la nova "pàgina", el focus es queda perdut i el <title> no canvia. Aquest és el repte de la lliçó següent, 07-02: Accessibilitat en aplicacions d'una sola pàgina (SPA), on el tauler de l'alumne de Cursalia serà el nostre banc de proves.
Curs d'Accessibilitat Web
Mòdul 1: Introducció a l'Accessibilitat Web
- Què és l'Accessibilitat Web?
- La Importància de l'Accessibilitat Web
- Visió General de les Lleis i Normes d'Accessibilitat
- Introducció a les WCAG
Mòdul 2: Comprensió de les Discapacitats i les Tecnologies d'Assistència
- Tipus de Discapacitats
- Visió General de les Tecnologies d'Assistència
- Com Utilitzen la Web les Persones amb Discapacitat
Mòdul 3: Principis del Disseny Accessible
- Perceptible: Fer el Contingut Disponible als Sentits
- Operable: Interfície d'Usuari i Navegació
- Comprensible: Informació i Operació
- Robust: Compatibilitat amb Tecnologies Actuals i Futures
Mòdul 4: Implementació de l'Accessibilitat en HTML i CSS
- HTML Semàntic
- Formularis Accessibles
- Ús de Rols i Propietats ARIA
- Contrast de Color i Redimensionament del Text
Mòdul 5: Accessibilitat en JavaScript i Multimèdia
- Creació de Widgets de JavaScript Accessibles
- Accessibilitat del Teclat
- Contingut de Vídeo i Àudio Accessible
- Proporcionar Alternatives de Text per a les Imatges
Mòdul 6: Proves i Avaluació de l'Accessibilitat
- Tècniques de Proves Manuals
- Eines de Proves Automatitzades
- Proves d'Usuari amb Tecnologies d'Assistència
- Interpretació d'Informes d'Accessibilitat
