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

  1. Quins elements són focusables de manera nativa
  2. tabindex: 0, -1 i per què mai positiu
  3. L'ordre del focus és l'ordre del DOM
  4. Focus visible i focus programàtic (element.focus())
  5. Trampes de teclat (criteri 2.1.2) i com evitar-les
  6. Skip links que funcionen de debò
  7. Tècnica: roving tabindex
  8. Tècnica: focus trap per a diàlegs modals
  9. Taula exhaustiva de convencions de tecles
  10. Errors comuns, exercicis i conclusió

  1. 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="…"> Només si té href
<button> També respon a Enter/Espai
<input>, <select>, <textarea> Excepte disabled
<div>, <span>, <p> No Necessiten tabindex
Element amb tabindex="0" Afegit manualment
Element amb contenteditable

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.

  1. tabindex: 0, -1 i per què mai positiu

tabindex 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 de Tab, però permet enfocar-lo per programació amb element.focus(). És imprescindible per portar el focus a un panell, a un encapçalament després de navegar, o per al roving tabindex.
  • tabindex positiu (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 un tabindex="1" perdut per desordenar tota la pàgina. Si necessites canviar l'ordre, reordena l'HTML.

  1. 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ó.

  1. 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-li tabindex="-1" primer, o el .focus() no tindrà efecte.

  1. 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 Tab i mai el deixa anar.
  • Un gestor de teclat que fa event.preventDefault() en Tab sense 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.

  1. 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à.

  1. 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.

  1. 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 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.

  1. 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

  • onclick en 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, afegeix tabindex="0" i gestiona Enter i Espai.
  • tabindex positiu. Trenca l'ordre del focus de tota la pàgina. Prohibit.
  • outline: none sense alternativa. Deixa els usuaris de teclat sense saber on són. Fes servir :focus-visible.
  • Cancel·lar Tab sense moure el focus. Crea una trampa (2.1.2). Si interceptes Tab, 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?

<a href="#main" class="skip-link">Salta al contingut</a>
<main id="main">…</main>

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.

© Copyright 2026. Tots els drets reservats