ARIA a fondo
En esta lección
- Cómo funciona ARIA: el árbol de accesibilidad
- Las cinco reglas de uso de ARIA
- Roles
- Estados y propiedades
- El nombre accesible
- aria-expanded y aria-controls
- Regiones vivas (live regions)
- Patrón: pestañas
- Patrón: botón de menú
- Patrón: ventana modal
- Ocultar contenido: hidden, aria-hidden e inert
- Errores frecuentes
- Resumen
En la lección de accesibilidad viste qué es ARIA, su primera regla (“si hay un elemento HTML nativo, úsalo”) y los atributos más comunes. Aquí vamos más allá: cómo funciona por dentro, cómo calcula el navegador el nombre que anuncia un lector de pantalla y cómo construir componentes que HTML no trae de serie, como unas pestañas, un menú desplegable o una ventana modal, de forma que se puedan usar con teclado y con lector de pantalla.
Cómo funciona ARIA: el árbol de accesibilidad
El navegador construye, a partir del HTML, un árbol de accesibilidad: una versión simplificada de la página en la que cada elemento tiene tres datos:
- Rol: qué es (botón, enlace, casilla, encabezado, pestaña…).
- Nombre: cómo se llama (“Enviar”, “Menú principal”).
- Estado: cómo está (marcado, expandido, desactivado, seleccionado…).
Es lo que leen los lectores de pantalla. Cuando un lector dice “Menú, botón, contraído”, está leyendo nombre, rol y estado.
Los atributos ARIA solo modifican ese árbol. No cambian el aspecto, no hacen que algo sea enfocable y no añaden comportamiento de teclado. Un <div role="button"> se anuncia como botón, pero sigue sin responder a Intro ni a Tab.
Consejo: puedes ver el árbol de accesibilidad en las DevTools de Chrome: pestaña Elementos, panel lateral Accesibilidad. Ahí aparecen el rol, el nombre calculado y los estados de cada elemento. Es la mejor forma de comprobar si tu ARIA hace lo que crees.
Las cinco reglas de uso de ARIA
- Usa HTML nativo siempre que puedas.
- No cambies la semántica nativa sin necesidad: nada de
<h2 role="tab">. - Todo control ARIA interactivo debe funcionar con teclado.
- No ocultes elementos enfocables con
role="presentation"oaria-hidden="true". - Todo elemento interactivo necesita un nombre accesible.
Roles
El atributo role dice qué es un elemento. Hay muchos, pero se agrupan así:
| Tipo | Ejemplos | Para qué |
|---|---|---|
| Regiones (landmarks) | banner, navigation, main, complementary, contentinfo, search, region | Las zonas de la página. Casi siempre se consiguen con <header>, <nav>, <main>, <aside>, <footer>, <search> y <section> con nombre |
| Widgets | button, tab, tabpanel, menu, menuitem, switch, slider, combobox, dialog | Controles interactivos |
| Estructura del documento | heading, list, listitem, table, img, group, separator | Partes del contenido. Casi siempre hay un elemento HTML equivalente |
| Regiones vivas | status, alert, log, timer | Zonas cuyo contenido cambia y debe anunciarse |
- Cada elemento HTML tiene un rol implícito (
<button>esbutton,<nav>esnavigation), así que<nav role="navigation">es redundante. - Un
rolesustituye al rol nativo:<a href="#" role="button">deja de anunciarse como enlace. - Algunos roles exigen un padre concreto: un
tabva dentro de untablist; unmenuitem, dentro de unmenu.
Estados y propiedades
Los atributos aria-* se dividen en dos grupos:
- Propiedades: describen algo que casi no cambia. Por ejemplo,
aria-label,aria-controls,aria-haspopupoaria-required. - Estados: describen algo que cambia al usar la página, normalmente desde JavaScript. Por ejemplo,
aria-expanded,aria-selected,aria-pressed,aria-checkedoaria-invalid.
Los estados más usados:
| Estado | Valores | Ejemplo de uso |
|---|---|---|
aria-expanded | true / false | Botón que abre y cierra un panel o un menú |
aria-pressed | true / false | Botón de alternar (“Mostrar solo vías libres”) |
aria-selected | true / false | Pestaña activa, opción elegida en una lista |
aria-checked | true / false / mixed | Casillas y interruptores hechos a medida |
aria-current | page, step, date, true… | Elemento actual de un grupo: página, paso de un proceso |
aria-invalid | true / false | Campo con un error de validación |
aria-disabled | true / false | Control desactivado que sigue siendo enfocable |
Por ejemplo, un <button aria-pressed="false">Solo vías libres</button> se anuncia como botón de alternar, y JavaScript cambia el valor a "true" al pulsarlo.
Cuidado: los valores de ARIA son textos, no booleanos de HTML.
aria-expandedsin valor no significatrue. Escribe siemprearia-expanded="true"oaria-expanded="false". Y al cambiarlo con JavaScript, usasetAttribute('aria-expanded', 'true').
El nombre accesible
El nombre accesible es el texto con el que se anuncia un elemento. Es lo más importante de ARIA: un botón sin nombre se anuncia solo como “botón”, y nadie sabe qué hace.
El navegador lo calcula siguiendo un orden de prioridad. Usa el primero que encuentra:
aria-labelledby: el texto de los elementos cuyosidindicas.aria-label: el texto que escribes en el atributo.- La fuente nativa de HTML: el
<label>de un campo, elaltde una imagen, el<caption>de una tabla, el<legend>de un<fieldset>… - El contenido del elemento, en roles que lo permiten (botones, enlaces, encabezados, pestañas).
- El atributo
title, como último recurso.
Mira qué nombre recibe cada uno:
<!-- Nombre: "Reservar clase" (contenido) -->
<button type="button">Reservar clase</button>
<!-- Nombre: "Cerrar" (aria-label gana al contenido "×") -->
<button type="button" aria-label="Cerrar">×</button>
<!-- Nombre: "Cuota mensual" (aria-labelledby gana a aria-label) -->
<h2 id="t-cuota">Cuota mensual</h2>
<section aria-labelledby="t-cuota" aria-label="Precios">...</section>
aria-labelledby: reutilizar texto visible
Es preferible a aria-label cuando el nombre ya está escrito en la página, porque si cambias el texto visible, el nombre cambia con él. Además, admite varios id separados por espacios, y los junta en orden:
<h3 id="curso-ini">Curso de iniciación</h3>
<p>Cuatro sesiones de dos horas.</p>
<a href="/cursos/iniciacion/" id="mas-ini" aria-labelledby="mas-ini curso-ini">Más información</a>
El enlace se anuncia como “Más información Curso de iniciación”. Así, en la lista de enlaces de la página, cada “Más información” se distingue de los demás.
Un detalle útil: aria-labelledby puede apuntar a un elemento oculto con hidden y aun así usar su texto.
aria-label: cuando no hay texto visible
Para botones con solo un icono, o para distinguir regiones iguales:
<button type="button" aria-label="Buscar">
<svg aria-hidden="true" width="20" height="20"><!-- lupa --></svg>
</button>
<nav aria-label="Principal">...</nav>
<nav aria-label="Migas de pan">...</nav>
El <svg> lleva aria-hidden="true" para que el lector no intente anunciarlo: el nombre ya lo da el botón.
Cuidado:
aria-labelno funciona bien en elementos sin rol interactivo ni de región, como un<div>o un<span>normales. Muchos lectores lo ignoran ahí. Úsalo en botones, enlaces, campos, regiones y widgets.
Y no lo uses para cambiar lo que dice un botón con texto visible: si el botón muestra “Enviar” y el aria-label dice “Mandar formulario”, quien usa control por voz dirá “pulsar Enviar” y no funcionará. El nombre accesible debe contener el texto visible.
aria-describedby: información extra
El nombre dice qué es; la descripción añade detalles. Se lee después del nombre, el rol y el estado, a menudo tras una pequeña pausa:
<label for="clave">Contraseña</label>
<input type="password" id="clave" aria-describedby="clave-req clave-error" aria-invalid="true">
<p id="clave-req">Al menos 12 caracteres.</p>
<p id="clave-error">La contraseña es demasiado corta.</p>
Al enfocar el campo, el lector dirá algo como: “Contraseña, editable, no válido. Al menos 12 caracteres. La contraseña es demasiado corta.”
aria-expanded y aria-controls
aria-expandeden el botón dice si lo que controla está abierto. Es imprescindible.aria-controlsen el botón apunta alidde lo que abre. Pocos lectores de pantalla lo aprovechan, pero no molesta y documenta la relación.
Este patrón se llama disclosure (mostrar y ocultar): un <button> con aria-expanded que muestra u oculta un <nav> con hidden. Es el correcto para la navegación de una web en el móvil, y tienes el ejemplo completo en accesibilidad. Si solo quieres mostrar y ocultar un bloque de texto, <details> y <summary> lo hacen sin JavaScript.
Regiones vivas (live regions)
Una región viva es una zona cuyo contenido se anuncia cuando cambia, aunque el foco esté en otro sitio. En accesibilidad viste aria-live="polite". Estas son todas las piezas:
| Atributo o rol | Qué hace |
|---|---|
aria-live="polite" | Anuncia el cambio cuando el lector termina lo que está diciendo |
aria-live="assertive" | Interrumpe para anunciarlo enseguida |
role="status" | Región polite para mensajes de estado (“Guardado”, “3 resultados”) |
role="alert" | Región assertive para errores importantes |
role="log" | Mensajes que se van añadiendo, como un chat |
aria-atomic="true" | Lee la región entera, no solo la parte que ha cambiado |
Un buscador que filtra mientras escribes:
<label for="buscar-via">Buscar vía</label>
<input type="search" id="buscar-via">
<p role="status" id="num-resultados"></p>
<ul id="lista-vias">...</ul>
<script>
const resultados = document.querySelector('#num-resultados');
// Tras filtrar la lista:
resultados.textContent = '4 vías encontradas';
</script>
Quien usa un lector de pantalla oye “4 vías encontradas” sin perder su sitio en el campo de búsqueda.
Cuidado: no abuses de
assertiveni derole="alert". Resérvalo para errores que impiden continuar.
Patrón: pestañas
Unas pestañas (tabs) muestran un panel de contenido cada vez. HTML no tiene un elemento para esto, así que se construyen con ARIA. Es el ejemplo de esta lección; pulsa “Pruébalo” para usarlo.
Estructura
<div role="tablist" aria-label="Días de la semana">
<button type="button" role="tab" id="tab-lunes" aria-selected="true" aria-controls="panel-lunes">Lunes</button>
<button type="button" role="tab" id="tab-martes" aria-selected="false" aria-controls="panel-martes" tabindex="-1">Martes</button>
</div>
<div role="tabpanel" id="panel-lunes" aria-labelledby="tab-lunes" tabindex="0">...</div>
<div role="tabpanel" id="panel-martes" aria-labelledby="tab-martes" tabindex="0" hidden>...</div>
tablistagrupa las pestañas y lleva un nombre.- Cada
tabes un<button>(así ya es enfocable y responde a Intro y espacio), conaria-selectedyaria-controls. - Cada
tabpaneltoma su nombre de su pestaña conaria-labelledby. Los paneles inactivos llevanhidden. tabindex="0"en el panel permite llegar a él con Tab cuando no tiene nada enfocable dentro.
Teclado: el roving tabindex
En unas pestañas, Tab no recorre todas las pestañas: entra en la pestaña activa y, con otro Tab, salta al panel. Para moverse entre pestañas se usan las flechas. Así, quien navega con teclado no tiene que pasar por diez pestañas para llegar al contenido.
Esto se consigue con la técnica del tabindex itinerante (roving tabindex): solo la pestaña activa tiene tabindex="0"; las demás, tabindex="-1". Al pulsar una flecha, JavaScript mueve el 0 a la nueva pestaña y le pone el foco.
| Tecla | Acción |
|---|---|
| Tab | Entra en la pestaña activa; el siguiente Tab va al panel |
| Flecha derecha / izquierda | Pestaña siguiente / anterior (dando la vuelta) |
| Inicio / Fin | Primera / última pestaña |
El código JavaScript completo, con las flechas, Inicio y Fin, está en el ejemplo de la lección.
Patrón: botón de menú
Cuidado con el nombre: en ARIA, role="menu" no es el menú de navegación de una web. Es un menú de aplicación, como el de “Archivo” de un editor: una lista de acciones que se recorre con las flechas. Si se lo pones a tu navegación, el lector esperará que se maneje con flechas y los enlaces dejarán de comportarse como enlaces. Para la navegación, usa el patrón disclosure de antes.
Un menú de acciones de verdad sería, por ejemplo, el botón “Acciones” de una reserva:
<button type="button" id="btn-acciones" aria-haspopup="menu" aria-expanded="false" aria-controls="menu-acciones">
Acciones
</button>
<ul role="menu" id="menu-acciones" aria-labelledby="btn-acciones" hidden>
<li role="menuitem" tabindex="-1">Cambiar fecha</li>
<li role="menuitem" tabindex="-1">Invitar a un amigo</li>
<li role="menuitem" tabindex="-1">Cancelar reserva</li>
</ul>
Lo que tienes que programar:
- Al abrir:
aria-expanded="true", quitarhiddeny poner el foco en el primermenuitem. - Flechas arriba y abajo para moverse entre opciones; Inicio y Fin para ir a la primera y la última.
- Intro o espacio ejecuta la opción y cierra el menú.
- Escape cierra el menú y devuelve el foco al botón.
- Al cerrar:
aria-expanded="false"yhidden.
aria-haspopup="menu" avisa de que el botón abre un menú, y el lector lo anuncia así (“Acciones, botón de menú, contraído”).
Patrón: ventana modal
Una modal es una ventana que bloquea el resto de la página hasta que la cierras. Hoy la forma correcta es el elemento <dialog> abierto con showModal(), que viste en elementos interactivos. Te da gratis casi todo lo que antes había que programar:
- Se anuncia con rol
dialogy como modal. - El resto de la página queda inerte: no se puede enfocar ni pulsar.
- Escape la cierra.
- Al abrirse, el foco entra en el diálogo; al cerrarse, vuelve al botón que la abrió.
Lo que sí te toca a ti es darle nombre:
<button type="button" id="abrir-baja">Darme de baja</button>
<dialog id="baja" aria-labelledby="baja-titulo" aria-describedby="baja-texto">
<h2 id="baja-titulo">¿Seguro que quieres darte de baja?</h2>
<p id="baja-texto">Perderás tu cuota de socio y las clases reservadas.</p>
<form method="dialog">
<button value="cancelar" autofocus>No, seguir siendo socio</button>
<button value="confirmar">Sí, darme de baja</button>
</form>
</dialog>
<script>
const baja = document.querySelector('#baja');
document.querySelector('#abrir-baja').addEventListener('click', () => baja.showModal());
</script>
aria-labelledbytoma el título como nombre del diálogo.aria-describedbyhace que el lector lea también la explicación al abrirlo.autofocuspone el foco en la opción segura (no borrar nada) cuando es una acción destructiva.
En código antiguo verás modales hechas con un <div role="dialog" aria-modal="true">. Funcionan, pero exigen programar a mano que el foco no salga de la ventana, que Escape la cierre y que el foco vuelva a su sitio. Con <dialog> no hace falta.
Ocultar contenido: hidden, aria-hidden e inert
| Técnica | ¿Se ve? | ¿Lo anuncia el lector? | ¿Se puede enfocar? |
|---|---|---|---|
hidden o display: none | No | No | No |
aria-hidden="true" | Sí | No | Sí (peligro) |
inert | Sí | No | No |
| Clase “solo para lectores” (CSS) | No | Sí | Sí |
aria-hidden="true" es solo para cosas decorativas, nunca para algo que contenga enlaces o botones. inert desactiva una zona entera (por ejemplo, lo que queda detrás de un panel lateral abierto). La clase “solo para lectores” (.sr-only) se explica en CSS accesible.
Errores frecuentes
- Poner
rolea un<div>y no programar el teclado. El lector anuncia “botón”, pero no funciona con Intro ni con Tab. Usa<button>. role="menu"en la navegación de la web. Cambia por completo cómo se maneja. Usa<nav>con una lista de enlaces y, si hace falta, un botón disclosure.aria-hidden="true"sobre algo enfocable. El usuario llega con Tab a un elemento “fantasma” que el lector no anuncia.- Olvidar actualizar los estados. Un
aria-expanded="false"que nunca cambia miente al usuario. Cada vez que algo se abre, se cierra o se selecciona, actualiza su atributo. aria-labelledbyapuntando a unidque no existe. El elemento se queda sin nombre. Revisa losidy que no estén repetidos.- Crear la región viva en el mismo momento que su mensaje. Debe existir antes, vacía, para que el cambio se anuncie.
Resumen
| Concepto | Qué recordar |
|---|---|
| Árbol de accesibilidad | Rol, nombre y estado de cada elemento; ARIA solo modifica esto |
role | Sustituye al rol nativo; no añade teclado ni foco |
| Propiedades | aria-label, aria-controls, aria-haspopup: casi no cambian |
| Estados | aria-expanded, aria-selected, aria-pressed…: actualízalos con JavaScript |
| Nombre accesible | aria-labelledby > aria-label > nativo (<label>, alt) > contenido > title |
aria-describedby | Información extra: requisitos, errores, ayudas |
| Regiones vivas | role="status" para avisos, role="alert" solo para errores urgentes |
| Pestañas | tablist, tab, tabpanel, aria-selected y roving tabindex con flechas |
| Botón de menú | aria-haspopup="menu", role="menu", flechas y Escape; no para la navegación |
| Modal | <dialog> con showModal() y aria-labelledby |
| Ocultar | aria-hidden solo en lo decorativo; inert para desactivar zonas |
| Comprobar | Panel Accesibilidad de las DevTools y un lector de pantalla |
En la siguiente lección verás cómo proteger tus páginas de ataques habituales: seguridad en HTML.
Pruébalo tú
Cambia el código y pulsa Ejecutar (o Ctrl + Enter).
Pon a prueba lo que has aprendido
¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.