wandres.dev
ELEMENTS IV · Accesibilidad en el inspector

Nombre, rol y estado: el algoritmo que casi nadie conoce

El orden de precedencia del cálculo del nombre accesible, cómo verlo en el panel, y por qué tu botón se anuncia con el texto equivocado.

⏱ 16 min

Todo elemento del árbol de accesibilidad se identifica por tres datos: qué es, cómo se llama, y en qué situación está. El rol y el estado suelen estar bien porque vienen del elemento; el nombre es donde se concentran los fallos, porque se calcula con un algoritmo de siete pasos con precedencias que casi nadie conoce y que produce resultados sorprendentes. El panel de accesibilidad muestra el resultado de ese cálculo y, lo que es más útil, la fuente de la que salió.

🎯 Al terminar esta lección sabrás
  • Recitar el orden de precedencia del cálculo del nombre accesible.
  • Leer en el panel de dónde procede el nombre de un elemento concreto.
  • Diagnosticar los cuatro casos en que el nombre calculado no es el que esperabas.
  • Distinguir nombre de descripción y saber cuándo usar cada uno.

El orden de precedencia

El algoritmo de cálculo del nombre accesible está especificado y el orden es este, de mayor a menor prioridad. En cuanto un paso produce un nombre no vacío, los siguientes no se consultan.

Uno: aria-labelledby. Toma el texto de los elementos referenciados por identificador, concatenado. Gana a todo lo demás, incluido el contenido del propio elemento. Puede referenciar varios identificadores separados por espacios, y puede referenciar elementos ocultos.

Dos: aria-label. Una cadena literal. Gana al contenido.

Tres: la etiqueta nativa del elemento. Para un control de formulario, el label asociado por for o por envoltura. Para una table, el caption. Para un fieldset, el legend. Para un img, el alt. Para un svg, el title que lleve dentro.

Cuatro: el contenido de texto del elemento. Solo para roles que lo permiten: botones, enlaces, encabezados, celdas, opciones. Un div con role="region" no toma su nombre del contenido.

Cinco: el atributo title. Es el último recurso, y depende de él es mala idea porque en móvil no hay hover y muchos lectores lo tratan de forma inconsistente.

Seis: placeholder, en el caso de los campos de texto. Peor todavía, porque el placeholder desaparece al escribir.

Si ninguno produce nada, el nombre es vacío y el lector anuncia solo el rol: “botón”, sin más.

⚠️
Cuidado

Los dos primeros pasos ganan al contenido visible, y ahí está el fallo de accesibilidad más extendido de la web moderna. Un botón que dice “Guardar cambios” en pantalla y tiene aria-label="Guardar" se anuncia como “Guardar”, y un usuario de control por voz que diga “pulsar guardar cambios” no activa nada, porque el nombre accesible no contiene ese texto. La regla de oro es que el nombre accesible debe contener el texto visible, y no simplemente ser distinto de él.

Leerlo en el panel

La sección de propiedades computadas de la pestaña de accesibilidad muestra el nombre calculado y, junto a él, de dónde salió. Esa segunda parte es la que resuelve las investigaciones.

Ver que el nombre viene de aria-label cuando esperabas que viniera del contenido explica de inmediato por qué el texto que estás leyendo en pantalla no es el que oye el lector. Ver que viene de title significa que todos los pasos anteriores fallaron y que hay que arreglar algo. Y ver el nombre vacío con la fuente ausente significa que ningún paso produjo nada.

Los cuatro casos que sorprenden

El icono que se cuela en el nombre. Un botón con un icono y texto, donde el icono es un span con una fuente de iconos o un carácter Unicode, produce un nombre que incluye ese carácter. En el mejor caso el lector lo ignora; en el peor lo anuncia por su nombre Unicode, produciendo cosas como “triángulo negro hacia la derecha Reproducir”. El arreglo es marcar el icono con aria-hidden="true", que es exactamente el caso de uso correcto de ese atributo.

El aria-labelledby que apunta a nada. Si el identificador referenciado no existe —porque el elemento aún no se ha renderizado, porque el identificador se genera dinámicamente, o por un error tipográfico— el paso uno produce cadena vacía y no cae al paso dos. El elemento se queda sin nombre pese a tener un aria-label perfectamente escrito debajo. Es un fallo silencioso y frecuente en componentes que se montan en varias fases.

El nombre concatenado con toda la fila. Un role="row" o un contenedor cuyo nombre se calcula desde el contenido acaba conteniendo el texto de todos sus descendientes, produciendo nombres de doscientos caracteres. Se ve inmediatamente en el panel y se arregla dando un nombre explícito o cambiando el rol.

El campo con placeholder como única etiqueta. Muy común en diseños minimalistas. El panel muestra el nombre procedente del placeholder, lo que ya es una señal, y el problema real es que al escribir el placeholder desaparece y el usuario pierde el contexto. Un label visualmente oculto pero presente en el árbol es la solución correcta.

Nombre frente a descripción

Son dos campos distintos y se calculan con algoritmos paralelos. El nombre identifica; la descripción amplía. Un lector anuncia el nombre siempre y la descripción normalmente después de una pausa, y algunos permiten desactivarla.

aria-describedby produce descripción, no nombre. aria-description también. Y title produce descripción si el nombre ya vino de otro sitio, o nombre si no había nada más, que es una de las ambigüedades más incómodas de la especificación.

La regla de reparto: el nombre responde a “qué es esto”, la descripción a “qué debo saber antes de usarlo”. El mensaje de error de un campo va en la descripción; su etiqueta, en el nombre.

Estados y propiedades

El tercer dato de la tripleta. Los estados cambian con la interacción y se exponen con atributos ARIA o con propiedades nativas.

aria-expanded para lo que se despliega, aria-checked y aria-selected para lo que se marca, aria-disabled para lo desactivado, aria-current para el elemento actual dentro de un conjunto, aria-pressed para los botones de alternancia, aria-invalid para los campos con error, aria-busy para las zonas que se están actualizando.

El error estructural con los estados es declararlos y no actualizarlos. Un aria-expanded="false" que nunca cambia a true es peor que no ponerlo, porque anuncia información falsa. La comprobación es trivial en el panel: despliega el menú y mira si el valor cambió.

// Vigila los cambios de estado ARIA de un elemento mientras interactuas
new MutationObserver(ms => {
  for (const m of ms) console.log(m.attributeName, '->', m.target.getAttribute(m.attributeName));
}).observe($0, { attributes: true, attributeFilter: ['aria-expanded', 'aria-checked', 'aria-selected', 'aria-pressed', 'aria-current', 'aria-invalid'] });

Con eso puesto sobre un componente, interactuar con él produce en la consola la traza exacta de qué estados cambian y cuándo. Si al abrir un menú no aparece nada, ya sabes que el estado no se está actualizando.

La primera regla de ARIA es no usar ARIA

La especificación de prácticas de autoría de ARIA abre con una regla que suena a provocación y es literal: si puedes usar un elemento HTML nativo con la semántica que necesitas, úsalo y no añadas ARIA. La razón no es purismo. Un button nativo trae de fábrica seis comportamientos que un div con role="button" no tiene y que hay que reimplementar uno por uno: es enfocable en el orden de tabulación sin tabindex; se activa con la barra espaciadora y con la tecla de entrada, que son dos manejadores distintos; expone su estado de desactivado y deja de ser enfocable cuando lo está; participa en el envío de formularios; responde a los modos de contraste forzado del sistema con estilos apropiados; y funciona con los modos de navegación de los lectores de pantalla que buscan controles por tipo. Cada uno de esos seis se puede reimplementar, y en la práctica los proyectos reimplementan dos o tres y se dejan el resto, produciendo componentes que pasan una auditoría automática y son inutilizables con teclado. En el panel esto se ve con una prueba de diez segundos que merece la pena convertir en reflejo: selecciona el componente, mira su rol computado, y luego intenta usarlo solo con el teclado. Si el rol dice botón pero la barra espaciadora no lo activa, tienes un div disfrazado. Y si te encuentras escribiendo role, tabindex, un manejador de keydown y tres atributos aria- para conseguir algo que un elemento nativo hace solo, ese es el momento exacto de parar y cambiar el elemento de la raíz. El caso legítimo de ARIA es el contrario: patrones que HTML no tiene —pestañas, árboles, rejillas de datos, regiones en vivo— donde no hay elemento nativo y ARIA es la única vía.