Lliçó 3 de 10 · 15 min de lectura · Revisada el 26/10/2026
Ginys compostos: 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.
Les barres d'eines, les pestanyes, els menús, les llistes d'opcions, els arbres i les graelles semblen components molt diferents, però comparteixen un mateix contracte de teclat: el component sencer és una sola parada de Tab i, un cop a dins, es navega amb les fletxes. L'APG (s'obre en una finestra nova) anomena aquests controls ginys compostos (components formats per diversos elements interactius que es comporten com un de sol) i descriu dues tècniques per gestionar-ne el focus: roving tabindex i aria-activedescendant. Després de la llista de reconstrucció d'un control aïllat que vas veure a «Botons, enllaços i controls personalitzats», aquí aprens la mecànica dels controls agrupats, que faràs servir a la resta del curs.
Per què una sola parada de tabulació
Si cadascun dels dotze botons de format d'un editor és una parada de Tab, cal prémer dotze vegades per arribar a l'àrea de text. Els sistemes operatius d'escriptori ho van resoldre fa dècades amb una convenció que les persones usuàries de teclat i de lector de pantalla coneixen bé: Tab salta entre components; les fletxes es mouen dins de cadascun. El rol del contenidor («barra d'eines», «llista de pestanyes», «arbre») avisa el lector de pantalla i la persona que aquesta convenció és vigent.
L'HTML natiu ja ho fa amb els botons d'opció (<input type="radio">) que comparteixen name: una sola parada i fletxes per canviar. Un giny compost amb ARIA reprodueix a mà aquest comportament. Els criteris implicats són 2.1.1 Teclat (tot s'ha de poder fer amb teclat), 2.4.3 Ordre del focus (el recorregut ha de ser lògic) i 4.1.2 Nom, funció i valor (rol, nom i estat exposats).
| Patró APG | Rol del contenidor | Fletxes que fa servir | Tècnica habitual |
|---|---|---|---|
| Barra d'eines | toolbar | Esquerra i dreta | Roving tabindex |
| Pestanyes | tablist | Esquerra i dreta (amunt i avall si és vertical) | Roving tabindex |
| Menú i barra de menús | menu, menubar | Amunt i avall; esquerra i dreta a la barra | Qualsevol de les dues |
| Llista d'opcions | listbox | Amunt i avall | Qualsevol de les dues |
| Combobox | combobox amb listbox | Amunt i avall | aria-activedescendant |
| Arbre i graella | tree, grid, treegrid | Totes quatre | Qualsevol de les dues |
Tècnica 1: roving tabindex
En el roving tabindex («tabindex itinerant») el focus del DOM es mou de debò d'un element a un altre. Només un element del grup té tabindex="0"; tots els altres tenen tabindex="-1", de manera que continuen sent enfocables per script però el navegador els salta en tabular. Quan la persona prem una fletxa, el teu codi posa -1 a l'element actual, 0 al de destinació i crida focus() sobre aquest últim.
<div role="toolbar" aria-label="Format del text">
<button type="button" aria-pressed="false" tabindex="0">Negreta</button>
<button type="button" aria-pressed="false" tabindex="-1">Cursiva</button>
<button type="button" aria-pressed="false" tabindex="-1">Subratllat</button>
</div>
const barra = document.querySelector('[role="toolbar"]');
const botones = [...barra.querySelectorAll('button')];
function mover(destino) {
botones.forEach((b) => b.tabIndex = (b === destino) ? 0 : -1);
destino.focus();
}
barra.addEventListener('keydown', (e) => {
const i = botones.indexOf(document.activeElement);
if (i === -1) return;
const n = botones.length;
const destinos = {
ArrowRight: botones[(i + 1) % n],
ArrowLeft: botones[(i - 1 + n) % n],
Home: botones[0],
End: botones[n - 1]
};
if (!destinos[e.key]) return;
e.preventDefault();
mover(destinos[e.key]);
});
barra.addEventListener('click', (e) => {
const b = e.target.closest('button');
if (b) mover(b);
});
Hi ha tres detalls importants. Primer, el tabindex="0" es queda a l'últim element visitat: si la persona surt amb Tab i torna amb Maj+Tab, aterra on ho havia deixat, que és el que l'APG recomana en la majoria de patrons. Segon, el gestor de clic sincronitza l'estat quan es fa servir el ratolí; sense això, la parada de Tab es queda en un botó que la persona ja no recorda. Tercer, preventDefault() evita que les fletxes desplacin la pàgina mentre es navega per la barra.
El gran avantatge: el navegador continua desplaçant l'element enfocat, aplicant :focus-visible i enviant als lectors de pantalla un esdeveniment de focus normal.
Tècnica 2: aria-activedescendant
Amb aria-activedescendant el focus del DOM no es mou: es queda al contenidor (o al camp de text, en un combobox), i un atribut indica quin dels seus elements està actiu. El lector de pantalla anuncia aquest element com si tingués el focus. El teu codi, en prémer una fletxa, només canvia el valor de l'atribut.
<p id="prov-t">Província de residència</p>
<ul role="listbox" tabindex="0" aria-labelledby="prov-t"
aria-activedescendant="op-murcia">
<li role="option" id="op-malaga" aria-selected="false">Màlaga</li>
<li role="option" id="op-murcia" aria-selected="true" class="activa">Múrcia</li>
<li role="option" id="op-navarra" aria-selected="false">Navarra</li>
</ul>
A canvi, assumeixes tres responsabilitats que el navegador ja no cobreix:
- L'indicador visual.
:focus-visibles'aplica al contenidor, no a l'opció activa. Has de pintar tu l'opció activa (amb una classe o amb el selector[aria-selected="true"]) amb un indicador que compleixi 2.4.7 Focus visible i tingui un contrast d'almenys 3:1 (1.4.11 Contrast no textual). - El desplaçament. Si la llista té barra de desplaçament, l'opció activa pot quedar fora de la vista. Crida
scrollIntoView({ block: 'nearest' })sobre ella a cada canvi, o incompliràs 2.4.11 Focus no ocult (mínim). - La referència vàlida. L'
idha d'existir, l'element ha de ser descendent del que té el focus (o pertànyer-li mitjançantaria-owns) i no pot estar amagat. En el combobox, l'especificació WAI-ARIA 1.2 (s'obre en una finestra nova) preveu expressament que apunti a una opció de la llista emergent que el camp controla.
Error freqüent
<ul role="listbox" tabindex="0" aria-activedescendant="opcion-3">
<li role="option" tabindex="0">Màlaga</li>
<li role="option" tabindex="0">Múrcia</li>
<li role="option" tabindex="0">Navarra</li>
</ul>Tres fallades alhora: les opcions no tenen id, així que aria-activedescendant apunta a un element que no existeix i el lector no anuncia res en prémer les fletxes; cada opció té tabindex="0", de manera que la llista es converteix en quatre parades de Tab; i es barregen les dues tècniques. Tria'n una i aplica-la sencera.
Quina cal triar
Cap no és «més accessible» que l'altra; cadascuna encaixa en situacions diferents. El roving tabindex és l'opció per defecte quan els elements són controls reals (botons, enllaços, pestanyes, elements de menú), perquè el navegador gestiona el focus, el desplaçament i l'indicador. aria-activedescendant és imprescindible quan el focus ha de romandre en un camp de text mentre es recorre una llista, com en el combobox que veuràs a la lliçó «Combobox, listbox i autocompletació»: si moguessis el focus a l'opció, la persona deixaria de poder escriure. També és útil en llistes llargues que es renderitzen a mesura que es desplacen. El seu suport als lectors de pantalla ha millorat, però prova'l en les combinacions del teu públic, sobretot al mòbil.
Infografia · Comparativa
Roving tabindex davant d'aria-activedescendant
Dues maneres de gestionar el focus dins d'un giny compost
Roving tabindex
- El focus del DOM es mou a cada element amb focus()
- Només l'element actiu té tabindex=0; la resta, -1
- El navegador desplaça la vista i aplica :focus-visible
- Per defecte: botons, pestanyes i elements de menú
aria-activedescendant
- El focus del DOM es queda al contenidor o al camp
- L'atribut apunta a l'id de l'element actiu
- Tu pintes l'indicador i desplaces la llista
- Imprescindible en combobox: es pot continuar escrivint
Llegeix la infografia com a text
Comparativa en dues columnes de les dues tècniques de l'APG per gestionar el focus dins d'un giny compost. Columna esquerra, roving tabindex: el focus del DOM es mou a cada element amb focus(); només l'element actiu té tabindex 0 i la resta té -1; el navegador desplaça la vista i aplica :focus-visible; és l'opció per defecte per a botons, pestanyes i elements de menú. Columna dreta, aria-activedescendant: el focus del DOM es queda al contenidor o al camp de text; l'atribut apunta a l'id de l'element actiu; qui desenvolupa pinta l'indicador visual i desplaça la llista; és imprescindible en el combobox, perquè permet continuar escrivint mentre es recorren els suggeriments.
Detalls que marquen la diferència
- Cicle o topall. En pestanyes i menús és habitual passar de l'últim al primer; en llistes, arbres i graelles, aturar-se a l'extrem. Una regla per patró, a tot el producte.
- Inici, Fi i escriptura anticipada. Inici i Fi porten al primer i a l'últim element. L'escriptura anticipada (type-ahead: teclejar una lletra per saltar al primer element que comença per aquesta lletra) és opcional en molts patrons, però l'APG la recomana a totes les llistes d'opcions, especialment a les de més de set.
- Elements inhabilitats. Fora dels ginys compostos, el que s'espera és que un control inhabilitat no rebi el focus. Dins d'un menú o una llista, l'APG assenyala que és habitual que continuï sent accessible amb fletxes (amb
aria-disabled="true"), perquè la persona sàpiga que l'opció existeix encara que no estigui disponible. - Elements que desapareixen. Si l'element actiu s'elimina (esborrar una etiqueta d'una llista de filtres), mou el focus al veí més proper. Un focus que cau a
<body>obliga a començar a tabular des del principi de la pàgina. - Orientació. Una llista de pestanyes vertical es declara amb
aria-orientation="vertical"i respon a les fletxes amunt i avall. - Mode dels lectors de pantalla. A Windows, NVDA i JAWS passen normalment al mode de focus (o mode formulari) en entrar a la majoria d'aquests rols, i a partir d'aquí les fletxes arriben al teu codi. Per això un rol de giny compost sense el teclat implementat deixa la persona atrapada: el lector ha deixat de fer servir les fletxes per llegir i el teu component tampoc no les fa servir.
Punt clau
Un giny compost és una promesa en dues parts: el rol anuncia «aquí es navega amb fletxes» i el teu JavaScript l'ha de complir. Si no implementaràs el teclat complet, no hi posis el rol; un grup de botons normals, cadascun amb la seva parada de Tab, és menys eficient però funciona.
Com comprovar-ho
- Recorre la pàgina amb Tab: cada giny compost ha de ser una única parada, i en sortir i tornar has d'aterrar a l'últim element visitat (o al seleccionat).
- Dins del component, prova totes les fletxes, Inici i Fi, i comprova que l'indicador visual acompanya sempre l'element actiu.
- A les eines de desenvolupament, observa com canvia
tabindexoaria-activedescendanti verifica que l'idreferenciat existeix i té nom. - Amb un lector de pantalla, cada fletxa ha de produir un anunci amb el nom de l'element, el seu rol i la seva posició («Cursiva, botó, no premut»).
- En llistes amb desplaçament, baixa fins al final amb fletxes: l'opció activa no ha de quedar mai amagada.
Prova-ho tu
Obre l'exemple de barra d'eines de l'APG. Entra amb Tab, mou-te amb les fletxes, surt amb Tab i torna amb Maj+Tab: fixa't en on aterres. Després obre l'inspector d'elements i observa com canvia l'atribut tabindex dels botons a cada pulsació.
Per aprofundir-hi: APG: desenvolupar la interfície de teclat.
Resum
- Un giny compost és una sola parada de Tab; a dins es navega amb fletxes, Inici i Fi.
- Roving tabindex: només l'element actiu té
tabindex="0", la resta-1, i el focus del DOM es mou ambfocus(). aria-activedescendant: el focus es queda al contenidor o al camp; tu pintes l'indicador, desplaces la llista i mantens unidvàlid.- Roving tabindex per defecte;
aria-activedescendantquan cal continuar escrivint, com en el combobox. - Si no implementes el teclat complet, no facis servir el rol del giny compost.
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.