Accesibilidad
En resumen: Accesibilidad significa que la página también funciona con el teclado y con un lector de pantalla: el navegador construye a partir del HTML un árbol con el rol, el nombre y el estado de cada elemento.
Problema
El botón que no es un botón
La página del producto funciona: haces clic en «Añadir al carrito» y el producto entra en el carrito. Un cliente escribe que no puede comprar nada. No usa ratón: se mueve con el teclado, y un lector de pantalla le dice lo que hay en la página.
Consola
Haz clic en el campo «Cantidad» y pulsa Tab. El foco se salta «Añadir al carrito» y va al corazón. El corazón es un botón, pero el lector de pantalla dice solo «botón»: qué hace, no se sabe. Los dos elementos se ven bien. El fallo está en lo que el navegador no dibuja.
La idea
El segundo árbol
A partir del DOM, el navegador construye otro árbol más: el árbol de accesibilidad. Es la página tal como la ven las personas y los programas que no miran la pantalla:
- un lector de pantalla le lee la página en voz alta a una persona que no puede ver;
- el control por voz: alguien dice «clic en Añadir al carrito», y el programa busca un botón con ese nombre;
- el teclado: Tab recorre los elementos que se pueden pulsar;
- las pruebas automáticas, como Testing Library, buscan por rol y nombre. El sandbox de aquí también comprueba así.
Cada nodo de este árbol tiene tres cosas:
- un rol: qué es el elemento: botón, enlace, encabezado, campo, imagen;
- un nombre accesible: cómo se llama: «Añadir al carrito»;
- un estado: si está pulsado, marcado o desactivado.
El rol viene de la etiqueta: <button> es un botón, <a href> es un enlace, <h1> es un encabezado. <div> y <span> no tienen rol. En el árbol solo queda su texto.
Míralo
Mira los dos árboles
Una ficha de producto con tres «botones». Fíjate a la derecha en lo que queda de cada uno.
<body><article> <h2>Pan</h2> <img src="bread.svg" alt="Pan"> <label>Cant. <input type="number"></label> <div onclick="add()">Añadir</div> <button><svg>…</svg></button> <button>Añadir</button></article></body>Paso a paso, en palabras
- A la izquierda está el DOM. A partir de él, el navegador construye un segundo árbol: el árbol de accesibilidad. Lo leen los lectores de pantalla, el control por voz y las pruebas por rol. <article> pasa a ser el rol article.
- <h2> pasa a ser heading (encabezado). Su nombre viene del texto de dentro: «Pan».
- Una imagen no tiene texto dentro. Su nombre viene de alt. Sin alt, la imagen no tiene nombre.
- El campo recibe el rol spinbutton (un campo numérico), y su nombre viene de <label>: «Cant.». El <label> en sí no está en el árbol, solo su texto.
- <div> no tiene rol. onclick no cambia eso. Solo queda el texto «Añadir». No es un botón, y el teclado no llega a él.
- Este es un botón de verdad, pero dentro solo hay un icono sin texto. El nombre está vacío. Un lector de pantalla dice solo «botón».
- Un <button> con texto: el rol button y el nombre «Añadir». Tab llega a él, y Enter y Espacio lo pulsan.
- getByRole('button', { name: 'Añadir' }) busca en el árbol de la derecha. Encuentra un botón: el de verdad. El <div> y el botón sin nombre no existen para esa búsqueda.
El árbol de accesibilidad se puede ver en el navegador: en las herramientas de desarrollo (F12) abre Elementos y el panel Accesibilidad. Muestra el rol y el nombre del elemento seleccionado.
La idea
De dónde sale el nombre
El nombre de un elemento se toma según reglas. Las fuentes más comunes:
- el texto de dentro:
<button>Añadir al carrito</button>,<a href="/cart">Carrito</a>,<h1>; - el
altde una imagen:<img src="logo.svg" alt="La tienda">. Un enlace con solo una imagen dentro toma su nombre dealt; - el
<label>de un campo: «Cantidad» es el nombre del campo de al lado; aria-label: un nombre escrito directamente en el atributo, cuando no hay texto visible:<button aria-label="Añadir a favoritos">con un icono dentro;aria-labelledby: el nombre es el texto de otro elemento, indicado por suid.
aria-label y aria-labelledby pesan más que el texto de dentro. El nombre dice lo que el elemento hace, no cómo se ve: «Añadir al carrito», no «carrito de la compra». Para una imagen dentro de un enlace, el nombre dice adónde lleva el enlace: «La tienda», no «logo».
Pruébalo
Ponles nombre
El logo de arriba lleva a la página de inicio, pero es una imagen sin alt. El botón del carrito es solo un icono. Ponle nombre al enlace («La tienda») y al botón («Añadir al carrito»). La comprobación los encuentra por rol y nombre y pulsa el botón.
Consola
Solución
index.html<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>Leche fresca 3% · La tienda</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<a href="/"><img src="img/logo.svg" alt="La tienda" width="40" height="40"></a>
</header>
<main>
<article>
<h1>Leche fresca 3%</h1>
<p>Precio: 2,40 €</p>
<button type="button" aria-label="Añadir al carrito" onclick="console.log('Añadido al carrito: Leche fresca 3%')">
<svg viewBox="0 0 24 24" width="22" height="22"><path d="M3 4h3l2 11h11l2-8H7" fill="none" stroke="currentColor" stroke-width="2"/><circle cx="9" cy="19" r="1.6"/><circle cx="18" cy="19" r="1.6"/></svg>
</button>
</article>
</main>
</body>
</html>
style.cssbody { font-family: system-ui, sans-serif; margin: 16px; }
.add, button { font: inherit; padding: 6px 14px; border: 0; border-radius: 6px; background: #1f6f5c; color: white; cursor: pointer; }
.fav { background: none; color: #c2477f; padding: 4px; }
.fav svg { fill: currentColor; }
.add { display: inline-block; }
label { display: block; margin: 8px 0; }
input { width: 4em; }
El logo recibe alt="La tienda", y el enlace toma su nombre de ahí. El botón recibe aria-label="Añadir al carrito": no tiene texto del que tomar un nombre. alt="logo" no pasa: quien oye «logo, enlace» no sabe adónde lleva. aria-label="Carrito" tampoco pasa: el botón no abre el carrito, añade a él.
La idea
El teclado y el foco
No todos usan ratón. A alguien le tiemblan las manos, alguien no ve el puntero, alguien escribe más rápido con el teclado. Se mueven por la página con Tab y pulsan con Enter.
El foco es el elemento que recibe las teclas ahora mismo. Tab lo mueve hacia delante, Mayús+Tab hacia atrás. Tab llega solo a los elementos que se pueden pulsar: enlaces con href, botones, campos, menús desplegables. El orden es el del DOM, de arriba abajo.
<button> lo trae todo: un rol, foco, y Enter y Espacio lo pulsan. <div> no tiene nada de esto. Darle foco y Enter es volver a escribir <button>.
tabindex cambia qué elemento recibe el foco:
tabindex="0": el elemento entra en el orden de Tab, en su lugar del DOM;tabindex="-1": Tab lo salta, pero el código puede darle foco confocus();tabindex="1"y más: el elemento va el primero, antes que todos los demás. El orden de la página se rompe. No lo uses.
El navegador dibuja un contorno alrededor del elemento con foco. Sin él, quien usa el teclado no sabe dónde está. No lo quites. Si no te gusta, sustitúyelo por otro.
Pruébalo
Con el teclado
«Añadir al carrito» es un <div>. Haz que Tab llegue a él después del campo «Cantidad» y que Enter añada al carrito. La comprobación pulsa Tab dos veces y luego Enter.
Consola
Solución
index.html<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>Pan Dobrudzha · La tienda</title>
<link rel="stylesheet" href="style.css">
<script src="script.js" defer></script>
</head>
<body>
<main>
<h1>Pan Dobrudzha</h1>
<p>Precio: 1,80 €</p>
<label>Cantidad <input type="number" value="1" min="1"></label>
<button type="button" class="add">Añadir al carrito</button>
</main>
</body>
</html>
style.cssbody { font-family: system-ui, sans-serif; margin: 16px; }
.add, button { font: inherit; padding: 6px 14px; border: 0; border-radius: 6px; background: #1f6f5c; color: white; cursor: pointer; }
.fav { background: none; color: #c2477f; padding: 4px; }
.fav svg { fill: currentColor; }
.add { display: inline-block; }
label { display: block; margin: 8px 0; }
input { width: 4em; }
script.jsconst add = document.querySelector('.add');
const qty = document.querySelector('input');
add.addEventListener('click', () => {
console.log(`Añadido al carrito: ${qty.value} × Pan Dobrudzha`);
});
<div class="add"> pasa a ser <button type="button" class="add">. La clase se queda, así script.js lo encuentra y el CSS lo dibuja como antes. Tab llega al botón después del campo porque está después en el DOM. Enter en un botón dispara click, así que el mismo listener funciona. <div role="button" tabindex="0"> no pasa: el lector de pantalla dice «botón» y Tab llega a él, pero Enter no hace nada. tabindex="1" en el botón tampoco pasa: Tab va primero a él, antes que al campo.
Pruébalo
Encuentra el error
El botón funciona con el ratón. Quien usa el teclado pulsa Tab y no ve dónde está. ¿Qué línea tiene la culpa?
Elige la línea con el error.
La línea 3 quita el contorno del foco. El botón recibe el foco, pero nada lo muestra. Borra la línea o pon otro contorno visible: button:focus-visible { outline: 3px solid #f4c542; }.
La idea
Encabezados y la primera regla de ARIA
Los encabezados son el índice de la página. Quien usa un lector de pantalla no lee la página desde el principio. Pide una lista de encabezados o salta entre ellos con una tecla (H en NVDA y JAWS). Por eso los niveles cuentan el esquema: un <h1> para la página, <h2> para las secciones, <h3> dentro de ellas. No elijas <h4> porque sea más pequeño. El tamaño se cambia con CSS.
ARIA (Accessible Rich Internet Applications) es un conjunto de atributos que cambian el árbol de accesibilidad: role, aria-label, aria-expanded, aria-live y otros. Le dicen al lector de pantalla algo que HTML no puede decir por sí solo.
ARIA cambia solo lo que se anuncia, no el comportamiento. role="button" en un <div> hace que el lector diga «botón», pero Tab sigue sin llegar a él y Enter sigue sin hacer nada. La persona oye «botón», pulsa, y no pasa nada. Eso es peor que un <div> sin rol.
Por eso la primera regla de ARIA es: si hay un elemento HTML que hace esto, úsalo. <button>, no role="button". <a href>, no role="link". <nav>, no role="navigation". ARIA es para los casos en que HTML no tiene un elemento así o no hay texto visible del que tomar un nombre.
Pruébalo
Completa
¿Qué atributo le da un nombre a un botón que solo tiene un icono?
<button type="button" ="Añadir a favoritos">
<svg viewBox="0 0 24 24">…</svg>
</button>aria-label: el nombre que dice el lector de pantalla y con el que el control por voz y las pruebas encuentran el botón.
Más precisamente, para curiosos
Mensajes que aparecen. Tras «Añadir al carrito», la página escribe «Añadido: Pan Dobrudzha». Quien mira la pantalla lo ve. El lector de pantalla no nota el cambio, porque el foco no está ahí. <p role="status"> (o aria-live="polite") dice: cuando cambie el texto de aquí, léelo al terminar lo que estás leyendo. Para errores urgentes existe role="alert", que interrumpe. El elemento tiene que estar en la página antes del mensaje: solo cambia su texto.
El idioma. <html lang="es"> le dice al lector de pantalla con qué voz leer. Con un lang equivocado, un texto en español se lee con pronunciación inglesa y no se entiende. Una palabra en otro idioma dentro del texto recibe el suyo: <span lang="fr">croissant</span>.
Ocultar. display: none y el atributo hidden quitan el elemento de la pantalla y del árbol de accesibilidad. aria-hidden="true" lo quita solo del árbol: sirve para iconos junto a un texto que el lector no tiene por qué decir. Un elemento con foco posible dentro de aria-hidden es un error: Tab llega a él, y el lector calla.
El orden de los nombres. El algoritmo completo es el estándar Accessible Name and Description Computation. En resumen: primero aria-labelledby, luego aria-label, luego lo que da HTML (<label>, alt, el texto de dentro) y al final title. title y placeholder son malos nombres: title solo aparece al pasar el ratón por encima, y placeholder desaparece en cuanto escribes.
:focus y :focus-visible. :focus se aplica siempre que el elemento tiene el foco, también tras un clic con el ratón. :focus-visible se aplica cuando el navegador decide que el contorno debe verse: con Tab, pero no tras un clic en un botón. Así que, si quieres cambiar el contorno, usa :focus-visible. CSS es el siguiente módulo.
Saltar al contenido. El primer enlace de muchas páginas es «Saltar al contenido» (<a href="#main">). Está oculto hasta que recibe el foco. Así quien usa el teclado no recorre todo el menú cada vez.
Ejercicio
El carrito
El carrito se hizo con prisas. «Carrito» es solo texto en negrita, los botones «×» son <span>, y el enlace «Ir al pago» tiene tabindex="1», «para que sea el primero». Arréglalo: un encabezado de nivel 1; botones a los que llegue Tab y que Enter pulse, con los nombres «Quitar Pan Dobrudzha» y «Quitar Leche fresca 3%»; Tab en el orden de la página: el primer botón, el segundo, el enlace. La comprobación recorre el carrito solo con el teclado.
Consola
Solución
index.html<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>Carrito · La tienda</title>
<link rel="stylesheet" href="style.css">
<script src="script.js" defer></script>
</head>
<body>
<main>
<h1>Carrito</h1>
<ul>
<li>Pan Dobrudzha, 2 × 1,80 € <button type="button" class="remove" aria-label="Quitar Pan Dobrudzha">×</button></li>
<li>Leche fresca 3%, 1 × 2,40 € <button type="button" class="remove" aria-label="Quitar Leche fresca 3%">×</button></li>
</ul>
<p>Total: 6,00 €</p>
<a href="/checkout">Ir al pago</a>
</main>
</body>
</html>
style.cssbody { font-family: system-ui, sans-serif; margin: 16px; }
.title { display: block; font-size: 1.6em; margin: 8px 0; }
li { margin: 6px 0; }
.remove { font: inherit; margin-left: 8px; padding: 0 8px; border: 1px solid #999; border-radius: 4px; background: white; cursor: pointer; }
script.jsfor (const remove of document.querySelectorAll('.remove')) {
remove.addEventListener('click', () => remove.closest('li').remove());
}
«Carrito» pasa a ser <h1>. Cada <span class="remove"> pasa a ser <button type="button" class="remove"> con aria-label: el texto visible «×» no dice qué hace el botón. El nombre dice también qué producto quita, porque dos botones «Quitar» suenan igual. tabindex="1" desaparece: el enlace está el último en el DOM y el último para Tab. La comprobación pulsa Enter en el primer botón y cuenta los productos, por eso <span role="button" tabindex="0"> no pasa.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: El documento HTML · Texto, enlaces, imágenes, tablas · Formularios
- Continúa en: Diseño responsive · Una lista a partir de datos · Datos desde una API · El primer componente
- Ejercicios sobre el tema: HTML: la estructura de la página