ARIA en la práctica: atributos y patrones que funcionan sin complicar el código

← Volver
Editor de código con sintaxis coloreada en pantalla oscura, entorno de desarrollo web
Foto de Rahul Mishra en Unsplash

ARIA (Accessible Rich Internet Applications) genera dos reacciones opuestas en los equipos de desarrollo: o se ignora por completo o se aplica en exceso hasta que la experiencia para los usuarios de lectores de pantalla empeora. Ninguna de las dos funciona.

Este post va al grano: qué es ARIA, cuándo usarlo de verdad, cuándo no hace falta, y tres patrones concretos con código limpio que puedes copiar y adaptar hoy.

1. Qué es ARIA y para qué sirve realmente

ARIA es un conjunto de atributos HTML definidos por el W3C como parte de la especificación WAI-ARIA. Su función es una sola: comunicar semántica a las tecnologías asistivas (lectores de pantalla, switches, braille) cuando el HTML nativo no es suficiente.

La especificación WAI-ARIA define tres categorías:

  • Roles: qué tipo de elemento es (role="dialog", role="alert", role="tablist").
  • Estados: la condición actual del elemento, y puede cambiar (aria-expanded, aria-checked, aria-disabled).
  • Propiedades: información adicional sobre el elemento, generalmente estable (aria-label, aria-describedby, aria-labelledby).

Lo que ARIA no hace: no cambia el aspecto visual del elemento, no añade comportamiento de teclado, no aplica estilos. Únicamente modifica el árbol de accesibilidad que el navegador expone a las tecnologías asistivas.

2. Cuándo NO necesitas ARIA

Antes de añadir cualquier atributo ARIA, la pregunta es: ¿existe ya un elemento HTML que haga esto?

PatrónSin ARIACon ARIA innecesario
Botón de acción<button>Enviar</button><div role="button">Enviar</div>
Enlace de navegación<a href="/servicios">Servicios</a><span role="link">Servicios</span>
Campo de formulario<input type="checkbox"><div role="checkbox">
Zona de navegación<nav><div role="navigation">
Sección principal<main><div role="main">

La columna de la derecha no está prohibida —hay casos extremos donde es necesaria—, pero en proyectos normales añade complejidad sin beneficio. Si usas <div role="button"> en lugar de <button>, además de ARIA también tienes que implementar manualmente el comportamiento de teclado (Enter, Space), el foco, y los estados. Con <button> lo tienes gratis.

El principio es: primero HTML semántico. ARIA complementa donde el HTML nativo no llega.

3. Los atributos que más vas a usar

aria-label

Proporciona un nombre accesible cuando no hay texto visible que lo haga. Su caso más claro: un botón con solo un icono SVG.

<!-- Sin texto visible: el lector de pantalla anunciaría "botón" sin más contexto -->
<button>
  <svg aria-hidden="true">...</svg>
</button>

<!-- Con aria-label: el lector anuncia "Cerrar menú, botón" -->
<button aria-label="Cerrar menú">
  <svg aria-hidden="true">...</svg>
</button>

Cuándo no usarlo: si hay texto visible que ya describe el elemento, aria-label lo sobreescribe. En ese caso usa aria-labelledby apuntando al id del texto visible, o simplemente confía en el contenido del elemento.

aria-describedby

Vincula un elemento con texto descriptivo adicional que ya existe en el DOM. La diferencia con aria-labelledby: el label es el nombre del elemento (conciso), la descripción es contexto extra (más detallado).

<label for="email">Email</label>
<input
  type="email"
  id="email"
  aria-describedby="email-hint email-error"
/>
<p id="email-hint">Usaremos este email para enviarte el informe.</p>
<p id="email-error" role="alert" hidden>Ese email no parece válido.</p>

Cuando el campo recibe foco, el lector de pantalla anuncia el label (“Email”) y después la descripción (“Usaremos este email…”). Si hay error, elimina el atributo hidden del párrafo de error y el lector lo anunciará también.

aria-live y las regiones dinámicas

Cuando el contenido de la página cambia sin recargarla (un mensaje de éxito, resultados de búsqueda, un contador de carrito), los lectores de pantalla no lo detectan automáticamente a menos que el área esté marcada como “live region”.

Dos valores principales:

  • polite: el lector de pantalla espera a que el usuario pare de interactuar antes de anunciar el cambio. Correcto para el 90% de los casos.
  • assertive: interrumpe lo que esté leyendo para anunciar de inmediato. Solo para alertas críticas que requieren atención inmediata.
<!-- Región polite: feedback no urgente (contadores, confirmaciones) -->
<div aria-live="polite" aria-atomic="true">
  <span id="cart-count">3</span> artículos en el carrito
</div>

<!-- Alternativa idiomática: role="status" tiene aria-live="polite" implícito -->
<div role="status">
  Formulario enviado correctamente.
</div>

<!-- role="alert" tiene aria-live="assertive" implícito -->
<div role="alert">
  Error: no se pudo completar el pago.
</div>

Un detalle técnico importante: la región live debe existir en el DOM antes de que se inyecte contenido en ella. Si la creas dinámicamente y la rellenas de golpe, algunos lectores de pantalla no la registran. La práctica habitual es renderizarla vacía al cargar la página y actualizar su contenido con JavaScript cuando sea necesario.

4. Tres patrones con código limpio

Patrón 1: Menú desplegable de navegación

Un menú desplegable de cuenta de usuario o un panel con enlaces de sección necesita comunicar su estado (abierto/cerrado) y la relación entre el botón disparador y el panel. Este es el caso más común y, contraintuitivamente, el que más se suele resolver mal: con role="menu" y role="menuitem". Veremos por qué eso es un error y cuál es el patrón correcto.

<button
  id="menu-btn"
  aria-expanded="false"
  aria-controls="menu-panel"
>
  Opciones
  <svg aria-hidden="true"><!-- icono flecha --></svg>
</button>

<ul id="menu-panel" hidden>
  <li><a href="/perfil">Mi perfil</a></li>
  <li><a href="/ajustes">Ajustes</a></li>
  <li><button>Cerrar sesión</button></li>
</ul>
const btn = document.getElementById('menu-btn');
const panel = document.getElementById('menu-panel');

btn.addEventListener('click', () => {
  const isOpen = btn.getAttribute('aria-expanded') === 'true';
  btn.setAttribute('aria-expanded', String(!isOpen));
  panel.hidden = isOpen;
});

El atributo aria-expanded debe actualizarse en JavaScript cada vez que cambia el estado. El lector de pantalla anuncia “Opciones, contraído” cuando está cerrado y “Opciones, expandido” cuando está abierto, y Tab recorre los enlaces internos como en cualquier otra lista.

Los modales son uno de los patrones más complejos en términos de accesibilidad: hay que gestionar el foco, bloquear la interacción con el fondo, y asegurarse de que al cerrarse el foco vuelve al elemento que lo abrió.

<button id="open-modal">Ver detalles del pedido</button>

<div
  id="order-modal"
  role="dialog"
  aria-modal="true"
  aria-labelledby="modal-title"
  aria-describedby="modal-desc"
  hidden
>
  <h2 id="modal-title">Detalles del pedido #1042</h2>
  <p id="modal-desc">Revisa la información antes de confirmar la cancelación.</p>

  <div><!-- Contenido del modal --></div>

  <button id="close-modal">Cerrar</button>
  <button>Confirmar cancelación</button>
</div>
const openBtn = document.getElementById('open-modal');
const modal = document.getElementById('order-modal');
const closeBtn = document.getElementById('close-modal');

openBtn.addEventListener('click', () => {
  modal.hidden = false;
  // Mueve el foco al primer elemento interactivo del modal
  closeBtn.focus();
});

closeBtn.addEventListener('click', () => {
  modal.hidden = true;
  // Devuelve el foco al elemento que abrió el modal
  openBtn.focus();
});

Para producción, añade también el focus trap (tecla Tab debe circular solo dentro del modal mientras está abierto) y el cierre con Escape. Hay librerías pequeñas y bien probadas para esto, como focus-trap, que evitan tener que reimplementar el comportamiento desde cero.

Patrón 3: Alerta dinámica en formulario

Cuando un formulario valida en tiempo real o muestra mensajes de error tras el envío, los usuarios de lector de pantalla necesitan que esos mensajes se anuncien.

<form id="contact-form" novalidate>
  <div class="field">
    <label for="nombre">Nombre</label>
    <input
      type="text"
      id="nombre"
      name="nombre"
      aria-required="true"
      aria-describedby="nombre-error"
      aria-invalid="false"
    />
    <span id="nombre-error" role="alert"></span>
  </div>

  <button type="submit">Enviar</button>

  <!-- Confirmación de envío exitoso -->
  <div role="status" id="form-status"></div>
</form>
const form = document.getElementById('contact-form');
const nombreInput = document.getElementById('nombre');
const nombreError = document.getElementById('nombre-error');
const formStatus = document.getElementById('form-status');

form.addEventListener('submit', (e) => {
  e.preventDefault();

  if (!nombreInput.value.trim()) {
    nombreInput.setAttribute('aria-invalid', 'true');
    nombreError.textContent = 'El nombre es obligatorio.';
    nombreInput.focus();
    return;
  }

  nombreInput.setAttribute('aria-invalid', 'false');
  nombreError.textContent = '';
  formStatus.textContent = 'Formulario enviado correctamente.';
});

El role="alert" en el span de error garantiza que el mensaje se anuncia en cuanto aparece. El role="status" en el mensaje de confirmación usa el valor polite: el lector espera a que el usuario esté quieto.

5. El checklist antes de añadir ARIA

Antes de escribir cualquier atributo ARIA, hazte estas cuatro preguntas:

  1. ¿Existe un elemento HTML nativo? Si sí, úsalo.
  2. ¿El elemento tiene ya un nombre accesible? Texto visible, <label> asociado, o alt en imágenes.
  3. ¿El estado cambia dinámicamente? Si sí, necesitas actualizar el atributo en JavaScript cada vez que cambia.
  4. ¿Has probado con un lector de pantalla real? VoiceOver (macOS/iOS), NVDA (Windows, gratuito) o TalkBack (Android) permiten verificar que el comportamiento es el esperado. Las herramientas automáticas como Lighthouse o axe detectan una parte de los errores, no todos.

La base sobre la que ARIA trabaja —semántica HTML correcta, contraste suficiente, navegación por teclado funcional— es el suelo firme que debe estar resuelto antes de añadir estos atributos.

6. Lo que ARIA no resuelve solo

Añadir atributos ARIA no es suficiente para tener una interfaz accesible. ARIA solo comunica semántica: si el botón del modal no recibe foco al abrirse, el role="dialog" no lo arregla. Si el carrusel avanza automáticamente sin que el usuario pueda pausarlo, aria-live solo empeora la experiencia.

La accesibilidad es una propiedad del sistema completo: HTML semántico, comportamiento de teclado, contraste, jerarquía visual y ARIA trabajan juntos. Cada capa hace su parte.

Si tienes una web con formularios complejos, menús anidados o contenido dinámico y quieres revisar cómo está funcionando para usuarios de tecnologías asistivas, cuéntame en qué punto estás y lo vemos juntos.