A la lliçó anterior vam construir widgets i, un cop i un altre, vam remetre "a 05-02" per a les mecàniques de teclat i focus. Ha arribat el moment. El teclat és la capa base de tota l'accessibilitat interactiva: si una cosa funciona amb teclat, gairebé sempre funciona també amb un lector de pantalla, un commutador o un control per veu. La Lucía navega exclusivament amb teclat per la seva mobilitat reduïda; la Marta fa servir NVDA, que s'apoia en el model de focus del navegador. Si el teu component no és operable amb teclat, per a elles simplement no existeix. En aquesta lliçó cobrim els fonaments: què és focusable, com funciona tabindex, com moure el focus sense robar-lo, com evitar les trampes de teclat, i dues tècniques clau —roving tabindex i focus trap— més la taula de referència de tecles que la resta del curs cita.
Contingut
- Quins elements són focusables de manera nativa
tabindex: 0, -1 i per què mai positiu- L'ordre del focus és l'ordre del DOM
- Focus visible i focus programàtic (
element.focus()) - Trampes de teclat (criteri 2.1.2) i com evitar-les
- Skip links que funcionen de debò
- Tècnica: roving tabindex
- Tècnica: focus trap per a diàlegs modals
- Taula exhaustiva de convencions de tecles
- Errors comuns, exercicis i conclusió
- Quins elements són focusables de manera nativa
Només un conjunt reduït d'elements HTML pot rebre el focus de teclat sense ajuda:
| Element | Focusable per defecte | Nota |
|---|---|---|
<a href="…"> |
Sí | Només si té href |
<button> |
Sí | També respon a Enter/Espai |
<input>, <select>, <textarea> |
Sí | Excepte disabled |
<div>, <span>, <p> |
No | Necessiten tabindex |
Element amb tabindex="0" |
Sí | Afegit manualment |
Element amb contenteditable |
Sí | — |
La lliçó aquí és contundent: fes servir elements natius. Un <button> és focusable, operable amb Enter i Espai, anunciat com a botó i compatible amb totes les tecnologies d'assistència sense escriure una línia de JavaScript. En canvi, un <div role="button"> t'obliga a afegir tabindex="0", gestionar Enter i Espai a mà i tot i així arrisques incompatibilitats. La primera regla d'ARIA del Mòdul 4 —"prefereix HTML natiu"— és, en el fons, una regla de teclat.
tabindex: 0, -1 i per què mai positiu
tabindex: 0, -1 i per què mai positiutabindex controla si un element entra en l'ordre de tabulació i la seva posició. Té tres usos, i només dos són bons:
<!-- tabindex="0": entra en l'ordre de tab en la seva posició natural del DOM -->
<div role="button" tabindex="0">Botó personalitzat</div>
<!-- tabindex="-1": NO entra en l'ordre de tab, però SÍ que és focusable per JS -->
<div id="panell" tabindex="-1">Panell al qual porto el focus amb .focus()</div>
<!-- tabindex="5": MAI. Trenca l'ordre natural. -->
<input tabindex="5">tabindex="0": fa focusable un element que no ho és, respectant l'ordre del DOM. Fes-lo servir per a widgets personalitzats.tabindex="-1": treu l'element del recorregut deTab, però permet enfocar-lo per programació ambelement.focus(). És imprescindible per portar el focus a un panell, a un encapçalament després de navegar, o per al roving tabindex.tabindexpositiu (1, 2, 3…): mai. Un valor positiu salta per davant de tot l'ordre natural, creant un recorregut de focus impredictible que és impossible de mantenir. N'hi ha prou amb untabindex="1"perdut per desordenar tota la pàgina. Si necessites canviar l'ordre, reordena l'HTML.
- L'ordre del focus és l'ordre del DOM
Quan l'usuari prem Tab, el focus avança seguint l'ordre del codi HTML, no l'ordre visual de la pantalla. Això té una conseqüència pràctica enorme: si fas servir CSS (order de flexbox, grid, position) per reordenar visualment els elements, el focus seguirà l'ordre del DOM i no coincidirà amb el que es veu.
/* PERILL: el segon element es veu primer, però el focus el visita després */
.barra { display: flex; }
.barra .accio-principal { order: -1; } /* salta a l'inici visualment */La Lucía veurà el botó principal a l'esquerra, però en tabular el focus anirà a un altre lloc primer. El criteri WCAG 2.4.3 (Ordre del focus) exigeix que l'ordre sigui lògic i significatiu. La regla segura: mantén l'ordre del DOM coherent amb l'ordre de lectura visual i fes servir CSS per a l'aspecte, no per reordenar el flux d'interacció.
- Focus visible i focus programàtic
Dues cares del mateix tema:
Focus visible. L'usuari de teclat ha de veure on és el focus en tot moment (criteri 2.4.7). L'error històric més greu de la web és outline: none sense reemplaçament. Com vam veure al Mòdul 4, la solució moderna és :focus-visible:
/* Correcte: anell de focus clar, només quan es navega amb teclat */
:focus-visible {
outline: 3px solid #1a73e8;
outline-offset: 2px;
}
/* MAI deixis això sense una alternativa visible: */
/* button:focus { outline: none; } ❌ */Focus programàtic. Amb element.focus() movem el focus des de JavaScript. És l'eina que fan servir tots els widgets del mòdul. Regles d'or:
// Correcte: moure el focus com a RESULTAT d'una acció de l'usuari
btnObrir.addEventListener('click', () => {
modal.hidden = false;
modal.querySelector('input').focus(); // l'usuari va demanar obrir → movem focus
});- Mai robis el focus sense que l'usuari l'hagi demanat. Moure el focus durant el scroll, en un
setInterval, o en carregar un anunci, desorienta i pot violar el criteri 3.2.1 (En rebre el focus) i 3.2.5. El focus es mou com a resposta a una acció, no per sorpresa. - Per enfocar un contenidor no interactiu (un panell, un
<h2>d'una secció nova) dona-litabindex="-1"primer, o el.focus()no tindrà efecte.
- Trampes de teclat (criteri 2.1.2) i com evitar-les
Una trampa de teclat passa quan el focus entra en un component i no pot sortir amb el teclat. L'usuari queda atrapat i, sense ratolí, no té escapatòria. El criteri 2.1.2 (Sense trampes per al focus del teclat) és de nivell A: és inacceptable. Causes típiques:
- Un widget de tercers (un editor de text enriquit, un mapa) que captura
Tabi mai el deixa anar. - Un gestor de teclat que fa
event.preventDefault()enTabsense reenviar el focus a cap altre lloc.
// TRAMPA: es cancel·la Tab però no es mou el focus enlloc → atrapat
editor.addEventListener('keydown', (e) => {
if (e.key === 'Tab') e.preventDefault(); // ❌ l'usuari no pot sortir
});Compte: un focus trap de modal és diferent i és legítim, perquè l'usuari sempre pot sortir amb Esc (ho veiem al punt 8). La diferència entre una trampa prohibida i un trap correcte és que el trap sempre ofereix una via d'escapament estàndard.
- Skip links que funcionen de debò
Un skip link ("Salta al contingut principal") permet a la Lucía i a la Marta evitar tabular per tota la capçalera i el menú a cada pàgina. És una àncora que ha de moure el focus de debò, no només fer scroll:
<a href="#main" class="skip-link">Salta al contingut principal</a>
<!-- … capçalera i navegació … -->
<main id="main" tabindex="-1">…</main>/* Amagat fins que rep el focus: apareix en tabular */
.skip-link {
position: absolute;
left: -9999px;
}
.skip-link:focus {
left: 1rem;
top: 1rem;
}El detall crític és el tabindex="-1" al <main>: sense ell, en alguns navegadors l'enllaç fa scroll però el focus es queda a dalt, i la següent pulsació de Tab torna al menú. Amb tabindex="-1", en activar l'enllaç el focus aterra dins de <main> i la navegació continua des d'allà.
- Tècnica: roving tabindex
En un grup d'elements relacionats (pestanyes, barra d'eines, grup de radios personalitzat) no volem que Tab visiti cadascun: seria esgotador. L'APG especifica que Tab entri al grup per una sola parada i que les fletxes moguin a dins. Això s'implementa amb roving tabindex: en tot moment, només un element del grup té tabindex="0" i la resta tabindex="-1"; en moure's amb les fletxes, "rodem" aquest 0.
const items = [...document.querySelectorAll('[role="tab"]')];
let indice = 0;
function moure(nouIndice) {
items[indice].tabIndex = -1; // l'anterior surt del tab
indice = (nouIndice + items.length) % items.length; // envolupant
items[indice].tabIndex = 0; // el nou entra al tab
items[indice].focus(); // i rep el focus
}
items.forEach((item, i) => {
item.tabIndex = i === 0 ? 0 : -1; // estat inicial
item.addEventListener('keydown', (e) => {
if (e.key === 'ArrowRight') { moure(indice + 1); e.preventDefault(); }
if (e.key === 'ArrowLeft') { moure(indice - 1); e.preventDefault(); }
if (e.key === 'Home') { moure(0); e.preventDefault(); }
if (e.key === 'End') { moure(items.length - 1); e.preventDefault(); }
});
});Així, la Lucía prem Tab una vegada per entrar a les pestanyes del panell de Cursalia i després fa servir les fletxes per recórrer-les, exactament com espera un usuari de lector de pantalla.
- Tècnica: focus trap per a diàlegs modals
Quan la matrícula (el modal de 05-01) està oberta, el focus ha de circular a dins del diàleg: en arribar a l'últim element i prémer Tab, tornem al primer; amb Shift+Tab al primer, saltem a l'últim. I Esc tanca. Implementació completa:
function activarTrap(modal, disparador) {
// Tots els elements focusables DINS del modal
const focusables = modal.querySelectorAll(
'a[href], button:not([disabled]), input:not([disabled]), select, textarea, [tabindex]:not([tabindex="-1"])'
);
const primer = focusables[0];
const ultim = focusables[focusables.length - 1];
primer.focus(); // focus inicial dins del diàleg
function alPrémer(e) {
if (e.key === 'Escape') {
tancar();
return;
}
if (e.key !== 'Tab') return;
// Circulació del focus: aquí està el "trap"
if (e.shiftKey && document.activeElement === primer) {
ultim.focus(); // Shift+Tab al primer → a l'últim
e.preventDefault();
} else if (!e.shiftKey && document.activeElement === ultim) {
primer.focus(); // Tab a l'últim → al primer
e.preventDefault();
}
}
function tancar() {
modal.hidden = true;
modal.removeEventListener('keydown', alPrémer);
disparador.focus(); // RETORNAR el focus a qui va obrir (imprescindible)
}
modal.addEventListener('keydown', alPrémer);
return tancar;
}Aquest trap sí que és correcte perquè Esc sempre ofereix sortida: no viola el 2.1.2. Nota moderna: l'element natiu <dialog> amb showModal() implementa gran part d'això per tu (focus atrapat i Esc), així que prefereix-lo quan puguis i reserva aquest codi per a navegadors o casos que ho requereixin.
- Taula exhaustiva de convencions de tecles
Aquesta és la taula de referència que 05-01 i el Mòdul 7 citen. Segueix les convencions de l'APG per tipus de widget:
| Widget | Tecla(es) | Acció esperada |
|---|---|---|
| Botó | Enter, Espai |
Activar |
| Enllaç | Enter |
Seguir l'enllaç (l'Espai NO l'activa; fa scroll) |
| Checkbox | Espai |
Marcar/desmarcar |
| Radio (grup) | ↑ ↓ ← → |
Moure i seleccionar dins del grup; Tab entra/surt del grup |
| Menú / menú botó | Enter/Espai/↓ obre; ↑ ↓ mou; Esc tanca; Home/End extrems |
Navegar opcions |
| Tabs (pestanyes) | ← → (o ↑ ↓ si vertical) mou; Home/End primera/última; Tab surt al panell |
Canviar de pestanya |
| Combobox / listbox | ↓/↑ obre i mou; Enter selecciona; Esc tanca; escriure filtra |
Triar opció del filtre |
| Acordió / disclosure | Enter, Espai |
Expandir/col·lapsar la secció |
| Diàleg modal | Esc tanca; Tab/Shift+Tab circulen a dins (focus trap) |
Operar i tancar |
| Slider (control lliscant) | ← → / ↑ ↓ pas; Home/End mín/màx; AvPàg/RePàg salt gran |
Ajustar valor |
Regla mnemotècnica: Enter/Espai activen, Esc cancel·la/tanca, les fletxes naveguen dins d'un grup, Home/End van als extrems. Si respectes aquestes convencions, la Marta i la Lucía se senten com a casa perquè coincideixen amb les de les aplicacions d'escriptori.
Errors Comuns i Consells
onclicken un<div>sense suport de teclat. El clic funciona amb ratolí però la Lucía no el pot activar. Fes servir<button>o, si no hi ha més remei, afegeixtabindex="0"i gestionaEnteriEspai.tabindexpositiu. Trenca l'ordre del focus de tota la pàgina. Prohibit.outline: nonesense alternativa. Deixa els usuaris de teclat sense saber on són. Fes servir:focus-visible.- Cancel·lar
Tabsense moure el focus. Crea una trampa (2.1.2). Si interceptesTab, reenvia sempre el focus a algun lloc. - Robar el focus. Moure el focus sense que l'usuari l'hagi demanat (en scroll, temporitzadors, càrrega d'anuncis) és desorientador i viola 3.2.x.
- Consell: prova cada pàgina desendollant el ratolí. Si pots arribar a tot, activar-ho tot i veure sempre on ets, has guanyat la meitat de la batalla d'accessibilitat.
Exercicis
Exercici 1. Explica la diferència entre tabindex="0" i tabindex="-1" i dona un cas d'ús real per a cadascun.
Exercici 2. El següent skip link fa scroll però el focus torna al menú a la següent pulsació de Tab. Què falta?
Exercici 3. Tens una barra d'eines amb cinc botons i vols que Tab entri per una sola parada i les fletxes moguin entre ells. Quina tècnica fas servir i quina és la seva idea central en una frase?
Solucions
Solució 1. tabindex="0" fica un element no focusable en l'ordre natural de tabulació (cas: un <div role="button"> personalitzat que ha de ser assolible amb Tab). tabindex="-1" el treu de l'ordre de Tab però permet enfocar-lo per JavaScript amb .focus() (cas: un panell o un <main> al qual portes el focus després d'una acció, o els elements inactius d'un roving tabindex).
Solució 2. Falta tabindex="-1" al <main>. Sense ell, l'àncora mou el scroll però no el focus, així que el focus roman a la capçalera i Tab torna al menú. Amb <main id="main" tabindex="-1">, en activar l'enllaç el focus aterra al contingut principal.
Solució 3. Es fa servir roving tabindex: només el botó "actiu" té tabindex="0" i els altres tabindex="-1"; en prémer les fletxes es trasllada aquest 0 (i el focus) al botó següent. Idea central en una frase: una sola parada de tabulació per a tot el grup; les fletxes mouen el focus a dins.
Conclusió
Ja domines la capa base: què és focusable, com es comporta tabindex, per què l'ordre del focus segueix el DOM, com moure el focus sense robar-lo, com evitar trampes de teclat, i les dues tècniques que sostenen els widgets del mòdul —roving tabindex i focus trap—, a més de la taula de tecles de referència. Amb això, els components de 05-01 queden completament operables sense ratolí. Hem fet accessible la interacció; ens falta fer accessible el contingut multimèdia. A la lliçó següent, 05-03: Contingut de Vídeo i Àudio Accessible, implementarem subtítols, transcripcions i audiodescripció sobre el reproductor de vídeo-lliçó de Cursalia, per al Diego, la Marta i el Hugo.
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
