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.
| Component | Fletxes | Tecles addicionals habituals |
|---|---|---|
Barra d'eines (toolbar) | Esquerra i dreta | Inici, Fi |
Llista d'opcions (listbox) | Amunt i avall | Inici, 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·la | Inici/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 pare | Inici, 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:
- L'indicador visual.
:focusno 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). - La referència vàlida. L'
idha 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 ambaria-controls. Si l'opció s'elimina (llistes virtualitzades que només pinten el que és visible), el lector es queda sense res per anunciar. - El desplaçament. Si la llista té barra de desplaçament, l'opció activa ha de quedar visible; d'aquí el
scrollIntoView.
Quina triar?
| Aspecte | Roving tabindex | aria-activedescendant |
|---|---|---|
| On és el focus del navegador | A l'element actiu | Al contenidor o al camp de text |
| Indicador visual | El dona :focus-visible | El dibuixes tu amb una classe |
| Compatibilitat | La més àmplia: és focus real | Bona en general, amb més diferències entre combinacions de lector i navegador |
| Usos típics | Barres d'eines, pestanyes, graelles, arbres, menús | Combobox amb camp de text, llistes en què el focus no s'ha de moure |
| Risc principal | Oblidar actualitzar els tabindex | Referè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'idexisteix 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, ifocus()a cada moviment. aria-activedescendant: el focus es queda al contenidor o al camp i l'atribut apunta a l'idde 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.