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

Lliçó 5 de 10 · 14 min de lectura · Revisada el 26/10/2026

Components compostos: navegació amb fletxes, roving tabindex i aria-activedescendant

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.

Fins ara hem tractat cada element interactiu com una parada de Tab. Funciona bé en una pàgina de contingut, però no en una barra d'eines de 12 botons, un calendari de 31 dies o una llista de 200 municipis: si cada element és una parada, travessar el component costa dotzenes de pulsacions, i qui fa servir un commutador hi triga minuts. Les aplicacions d'escriptori van resoldre això fa dècades amb una convenció que la guia de pràctiques d'autoria d'ARIA (APG (s'obre en una finestra nova)) del W3C recull per al web: Tab entra i surt del component, i les fletxes es mouen a dins. Aquesta lliçó explica aquesta convenció i les dues tècniques per implementar-la: roving tabindex i aria-activedescendant. Els patrons concrets (menús, pestanyes, carrusels) els veuràs al curs de components interactius; aquí aprendràs el mecanisme que comparteixen tots.

La convenció: Tab entre components, fletxes a dins

Un component compost és un control format per diversos elements que es comporten com una unitat: una barra d'eines, una llista d'opcions, un grup de botons d'opció, una llista de pestanyes, una graella de dades o un arbre. Les regles generals són:

  • Tot el component és una sola parada a la seqüència de Tab.
  • A dins, les fletxes mouen el focus segons l'orientació del component; Inici i Fi van al primer i a l'últim element.
  • En llistes i arbres llargs, escriure una lletra porta a l'element següent que comença per aquesta lletra. És una drecera d'una sola tecla, però compleix el 2.1.4 Dreceres de teclat perquè només és activa quan el component té el focus, com vam veure a la primera lliçó.
  • En sortir amb Tab i tornar-hi, el focus hauria de caure a l'últim element actiu o a l'element seleccionat, no sempre al primer.

Ja coneixes un component compost natiu: el grup de botons d'opció (input type="radio" amb el mateix name). Tab entra a l'opció marcada, o a la primera si no n'hi ha cap; les fletxes canvien la selecció; una altra pulsació de Tab surt del grup. El navegador ho fa sol. Els components personalitzats han de reproduir aquest comportament amb codi.

ComponentFletxesTecles addicionals habituals
Barra d'eines (toolbar)Esquerra i dretaInici, Fi
Llista d'opcions (listbox)Amunt i avallInici, Fi, escriure per saltar
Grup d'opció (radiogroup)Les quatre; a més, marquen l'opció—
Graella o calendari (grid)Les quatre, cel·la a cel·laInici/Fi de fila; en un calendari, Re Pàg/Av Pàg canvien de mes
Arbre (tree)Amunt i avall; dreta expandeix, esquerra contrau o puja al node pareInici, Fi, escriure per saltar

Tècnica 1: roving tabindex

En el roving tabindex («tabindex itinerant») el focus del navegador es mou de debò entre els elements. Només un té tabindex="0" i és el que rep el focus en entrar amb Tab; els altres tenen tabindex="-1", de manera que Tab no s'hi atura. En prémer una fletxa, el codi passa el 0 a l'element de destinació, posa -1 a l'anterior i crida focus().

<div role="toolbar" aria-label="Format del text" aria-controls="editor">
  <button type="button" tabindex="0" aria-pressed="false">Negreta</button>
  <button type="button" tabindex="-1" aria-pressed="false">Cursiva</button>
  <button type="button" tabindex="-1" aria-pressed="false">Subratllat</button>
  <button type="button" tabindex="-1">Insereix un enllaç</button>
</div>
const barra = document.querySelector('[role="toolbar"]');
const botones = [...barra.querySelectorAll('button')];

barra.addEventListener('keydown', (e) => {
  const actual = botones.indexOf(document.activeElement);
  let destino = null;
  if (e.key === 'ArrowRight') destino = (actual + 1) % botones.length;
  if (e.key === 'ArrowLeft') destino = (actual - 1 + botones.length) % botones.length;
  if (e.key === 'Home') destino = 0;
  if (e.key === 'End') destino = botones.length - 1;
  if (destino === null) return; // Tab, Retorn i Espai segueixen el seu curs

  e.preventDefault(); // que les fletxes no desplacin la pàgina
  botones[actual].tabIndex = -1;
  botones[destino].tabIndex = 0;
  botones[destino].focus();
});

Com que el focus és real, tota la resta funciona sense esforç: :focus-visible dibuixa l'indicador, el lector de pantalla anuncia el botó enfocat, l'ampliador de pantalla el segueix i, en sortir i tornar amb Tab, el navegador torna a l'últim botó actiu perquè és l'únic amb tabindex="0". Per això és la tècnica recomanada per defecte.

En aplicacions amb React, Vue o Angular, desa a l'estat del component quin element és l'actiu. Si el component es torna a renderitzar i tots els elements recuperen el tabindex inicial, el focus torna sempre al primer o, pitjor, cap no queda amb tabindex="0" i el component desapareix de la seqüència.

Tècnica 2: aria-activedescendant

De vegades el focus del navegador s'ha de quedar en un lloc. El cas típic és un combobox (un camp de text amb una llista de suggeriments): si en baixar amb la fletxa el focus passés a l'opció, la persona ja no podria continuar escrivint. La solució és aria-activedescendant: el focus es queda al camp, i aquest atribut apunta a l'id de l'opció activa. El lector de pantalla anuncia aquesta opció com si tingués el focus.

<label for="municipio">Municipi</label>
<input id="municipio" type="text" role="combobox"
       aria-autocomplete="list" aria-expanded="true"
       aria-controls="lista-municipios" aria-activedescendant="mun-2">
<ul id="lista-municipios" role="listbox" aria-label="Municipis">
  <li id="mun-1" role="option">Lorca</li>
  <li id="mun-2" role="option" aria-selected="true" class="activa">Lorquí</li>
  <li id="mun-3" role="option">Los Alcázares</li>
</ul>
function activar(opcion) {
  const anterior = lista.querySelector('.activa');
  if (anterior) {
    anterior.classList.remove('activa');
    anterior.removeAttribute('aria-selected');
  }
  opcion.classList.add('activa');
  opcion.setAttribute('aria-selected', 'true');
  campo.setAttribute('aria-activedescendant', opcion.id);
  opcion.scrollIntoView({ block: 'nearest' }); // que l'opció quedi a la vista
}

Amb aquesta tècnica assumeixes tres responsabilitats que el navegador ja no cobreix:

  1. L'indicador visual. :focus no s'aplica a l'opció, perquè el focus és al camp. Has de dibuixar el ressaltat amb una classe, i aquest ressaltat ha de complir el 2.4.7 Focus visible i tenir prou contrast (1.4.11 Contrast no textual).
  2. La referència vàlida. L'id ha d'existir al DOM i pertànyer al component: un descendent de l'element amb focus o, en un combobox, la llista que aquest controla amb aria-controls. Si l'opció s'elimina (llistes virtualitzades que només pinten el que és visible), el lector es queda sense res per anunciar.
  3. El desplaçament. Si la llista té barra de desplaçament, l'opció activa ha de quedar visible; d'aquí el scrollIntoView.

Quina triar?

AspecteRoving tabindexaria-activedescendant
On és el focus del navegadorA l'element actiuAl contenidor o al camp de text
Indicador visualEl dona :focus-visibleEl dibuixes tu amb una classe
CompatibilitatLa més àmplia: és focus realBona en general, amb més diferències entre combinacions de lector i navegador
Usos típicsBarres d'eines, pestanyes, graelles, arbres, menúsCombobox amb camp de text, llistes en què el focus no s'ha de moure
Risc principalOblidar actualitzar els tabindexReferències a id inexistents i manca d'indicador

Error freqüent

Tres variants del mateix problema:

  • Posar tabindex="0" a cada element del component: un calendari amb 31 parades o una barra amb 12. Tècnicament operable, però esgotador.
  • Implementar les fletxes sense e.preventDefault(): el focus canvia d'opció i, alhora, la pàgina es desplaça.
  • Convertir el menú de navegació del lloc, que és una simple llista d'enllaços, en un component amb role="menu" i fletxes. Qui hi arriba espera tabular per enllaços; el lector anuncia un menú d'aplicació i canvia les regles. Per a la navegació principal n'hi ha prou amb enllaços i botons de desplegament.

Prova-ho tu

Obre la pàgina Developing a Keyboard Interface de l'APG i, des d'allà, l'exemple del patró Toolbar. Entra amb Tab, mou-te amb les fletxes, surt amb Tab i torna amb Maj+Tab. Compta les parades de Tab que necessites per travessar la barra i comprova en quin botó caus en tornar.

Com comprovar-ho

  • Compta les parades: cada component compost ha de ser una sola parada de Tab.
  • A dins, prova totes les fletxes, Inici i Fi, i comprova que la pàgina no es desplaça alhora.
  • Surt i torna: el focus ha de caure a l'element actiu o al seleccionat.
  • Amb aria-activedescendant, verifica a l'inspector que l'atribut canvia a cada pulsació, que l'id existeix i que el ressaltat es veu.
  • Comprova amb un lector de pantalla que cada moviment s'anuncia (ho treballaràs a fons al nivell d'auditoria).

Punt clau

Un component compost és una parada de Tab i es recorre amb fletxes. Fes servir roving tabindex sempre que puguis, perquè el focus és real; reserva aria-activedescendant per a quan el focus s'hagi de quedar en un camp, i llavors dibuixa tu l'indicador.

Resum

  • Tab entra i surt del component; les fletxes, Inici i Fi es mouen a dins; escriure una lletra pot saltar a un element.
  • Roving tabindex: un únic element amb tabindex="0", la resta amb -1, i focus() a cada moviment.
  • aria-activedescendant: el focus es queda al contenidor o al camp i l'atribut apunta a l'id de l'opció activa; l'indicador visual i el desplaçament són cosa teva.
  • Recorda l'última posició en sortir i tornar, i fes servir preventDefault() a les fletxes que gestiones.
  • No converteixis en component compost el que és una llista d'enllaços.

Portes 3 lliçons d'aquest curs: que no es perdin

Crea el teu compte gratis i desa el que ja has llegit 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.