Ir al contenido
angel.library

Atributos ARIA — nombres, estados y relaciones

ARIA significa Accessible Rich Internet Applications o aplicaciones de internet enriquecidas accesibles. Sus atributos comunican al árbol de accesibilidad el nombre, rol, estado o relación de un elemento cuando HTML no puede expresarlos por sí solo.

ARIA no agrega comportamiento, foco, teclado, estilos ni validación. Escribir <div role="button"> hace una promesa: el código todavía debe implementar activación con teclado, foco, estado deshabilitado y todos los comportamientos de un botón. Por eso, <button> sigue siendo la primera opción.

<!-- Correcto: semántica y teclado incorporados. -->
<button type="button">Guardar</button>
<!-- Evitar: obliga a reconstruir el comportamiento nativo. -->
<div
role="button"
tabindex="0"
>
Guardar
</div>

Antes de agregar ARIA, sigue este orden:

  1. Busca un elemento HTML con la semántica correcta.
  2. Proporciona texto visible mediante label, legend, caption o contenido del control.
  3. Usa atributos HTML nativos como required, disabled, readonly, open o alt.
  4. Agrega ARIA únicamente para completar información que HTML no expresa.
  5. Prueba nombre, rol, estado, foco y teclado con tecnología asistiva.

ARIA incorrecto puede comunicar una interfaz distinta de la que aparece en pantalla. Un atributo no debe agregarse “por si acaso”.

El nombre accesible identifica un elemento de forma breve. La descripción accesible proporciona ayuda o contexto adicional. No son intercambiables.

TécnicaCuándo usarlaEjemplo
Texto internoBotón o enlace con texto visible<button>Guardar</button>
<label for>Nombre visible de un control de formulario<label for="email">Correo</label>
<legend>Nombre de un grupo dentro de fieldsetMétodos de pago
altAlternativa de una imagen informativa<img alt="Ventas por trimestre">
aria-labelledbyEl texto visible ya existe en otro elementoDiálogo nombrado por su título
aria-labelNo existe texto visible apropiado para nombrar el controlBotón que solo muestra un icono
aria-describedbyAyuda, formato, requisito o error adicionalInstrucciones de contraseña
aria-detailsExplicación extensa o estructurada que debe poder recorrerseDescripción detallada de un gráfico
<button
type="button"
aria-label="Cerrar diálogo"
>
<svg
aria-hidden="true"
focusable="false"
>
<!-- icono X -->
</svg>
</button>

Úsalo cuando el control necesita nombre pero el diseño no contiene texto visible adecuado, como un botón de icono. El valor describe el propósito —“Cerrar diálogo”—, no la apariencia —“Icono X”—.

Evita aria-label cuando ya existe un texto visible que puede utilizarse. El valor es invisible, puede quedar sin traducir y puede sobrescribir el nombre que el elemento obtenía de su contenido.

<!-- Evitar: la pantalla dice Guardar y el lector anuncia Enviar formulario. -->
<button aria-label="Enviar formulario">Guardar</button>
<!-- Correcto: el texto visible ya proporciona el nombre. -->
<button>Guardar</button>

Cuando existe una etiqueta visible, el nombre accesible debe contener ese texto. Esto permite que una persona que utiliza control por voz diga la palabra que está viendo.

No uses aria-label para agregar explicaciones a párrafos, div, listas o encabezados. Muchos elementos estáticos no deben recibir un nombre accesible y algunos lectores pueden ignorarlo o presentar información confusa.

aria-labelledby: nombrar con texto existente

Sección titulada «aria-labelledby: nombrar con texto existente»

aria-labelledby recibe uno o varios identificadores separados por espacios. El texto de esos elementos forma el nombre en el mismo orden.

<section
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
>
<h2 id="dialog-title">Eliminar proyecto</h2>
<p>Esta acción no se puede deshacer.</p>
</section>

Es preferible a repetir el título dentro de aria-label: el texto visible y el nombre accesible permanecen sincronizados. Cada id debe existir y ser único en el documento.

<span id="billing-label">Dirección</span>
<span id="billing-context">de facturación</span>
<div
role="group"
aria-labelledby="billing-label billing-context"
>
<!-- campos -->
</div>

Para formularios, un <label> nativo sigue siendo mejor porque al pulsarlo también se activa o enfoca el control. aria-labelledby comunica el nombre, pero no agrega esa interacción.

<label for="password">Contraseña</label>
<p id="password-help">Usa al menos 12 caracteres.</p>
<p id="password-error">La contraseña es demasiado corta.</p>
<input
id="password"
type="password"
aria-describedby="password-help password-error"
aria-invalid="true"
/>

El control continúa llamándose “Contraseña”; después puede anunciar la ayuda y el error. No coloques el nombre principal únicamente en aria-describedby.

Si la explicación contiene listas, enlaces o una tabla que la persona debe explorar, considera aria-details. El soporte puede variar, por lo que el contenido también debe permanecer visible y cercano.

El atributo role cambia cómo se presenta un elemento a tecnologías asistivas. No lo uses para repetir la semántica que HTML ya incluye.

<!-- Redundante. -->
<button role="button">Comprar</button>
<!-- Preferible. -->
<button>Comprar</button>
NecesidadHTML preferidoEvita reemplazarlo por
Acción<button><div role="button">
Navegación<a href="…"><span role="link">
Casilla<input type="checkbox"><div role="checkbox">
Encabezado<h1>–<h6><div role="heading" aria-level="2">
Navegación principal<nav><div role="navigation">
Contenido principal<main><div role="main">

Roles como tab, menu, tree, grid o combobox representan widgets completos. Cada patrón tiene estructura, relaciones y comportamiento de teclado propios. Un menú ARIA no es cualquier lista de enlaces y unas pestañas no son solo botones alineados.

El estado ARIA debe reflejar la interfaz en este momento. Si el panel se cierra, aria-expanded debe cambiar a false; si un error se corrige, aria-invalid debe retirarse o volver a false.

AtributoQué comunicaSe coloca en
aria-expandedEl contenido controlado está abierto o cerradoBotón o control que expande
aria-controlsIdentificador del elemento controladoControl disparador
aria-currentElemento actual dentro de un conjuntoEnlace, paso, fecha o página actual
aria-selectedOpción seleccionada en un widget compuestotab, option, row, gridcell compatibles
aria-checkedEstado de casilla, radio o switchElemento con rol compatible
aria-pressedEstado de un botón conmutadorBotón que permanece activo o inactivo
aria-disabledEl elemento se presenta como no disponibleElemento que debe seguir perceptible
aria-readonlyEl valor se puede leer, pero no editarCampo o widget compatible
aria-requiredEl valor es obligatorioControl de formulario compatible
aria-invalidEl valor actual no cumple las reglasControl que contiene el error
aria-errormessageIdentificador del mensaje de errorControl inválido
aria-haspopupEl control abre un menú, listbox, árbol, grid o diálogoBotón disparador
aria-modalEl diálogo bloquea la interacción exteriorContenedor con rol dialog o alertdialog
aria-busyUna región todavía está actualizándoseContenedor que recibe cambios
<button
type="button"
id="faq-button"
aria-expanded="false"
aria-controls="faq-panel"
>
¿Cómo cambio mi contraseña?
</button>
<div
id="faq-panel"
hidden
>
<!-- respuesta -->
</div>
const expanded = button.getAttribute("aria-expanded") === "true"
button.setAttribute("aria-expanded", String(!expanded))
panel.hidden = expanded

aria-expanded describe el estado; hidden controla la visibilidad. aria-controls expresa la relación, pero no abre el panel ni mueve el foco.

Estos estados se parecen, pero representan conceptos diferentes:

  • aria-current="page": enlace de la página actual dentro de una navegación.
  • aria-selected="true": pestaña u opción seleccionada dentro de un widget.
  • aria-checked="true": casilla, radio o switch marcado.
  • aria-pressed="true": botón conmutador activado.
<nav aria-label="Cuenta">
<a
href="/profile"
aria-current="page"
>Perfil</a
>
<a href="/security">Seguridad</a>
</nav>
<button
type="button"
aria-pressed="false"
>
Silenciar
</button>

En un botón con aria-pressed, conserva un nombre estable como “Silenciar” y deja que el estado anuncie si está presionado. Si cambias el texto a “Activar sonido”, normalmente se trata como una acción diferente y puede no necesitar aria-pressed.

<button disabled>Guardar</button>

disabled impide activación y elimina controles de formulario del orden de foco. Es la opción preferida cuando está disponible.

<a
href="/next"
aria-disabled="true"
>Siguiente</a
>

aria-disabled="true" solo comunica el estado. El código todavía debe impedir la acción, ajustar el estilo y decidir si el elemento permanece en el orden de foco. No lo uses como una protección de permisos: el servidor debe autorizar la operación.

Prefiere required, readonly y disabled nativos en controles HTML. Agrega ARIA cuando un widget personalizado lo necesite o para exponer relaciones adicionales.

<label for="phone">Teléfono</label>
<input
id="phone"
name="phone"
required
aria-invalid="true"
aria-describedby="phone-error"
/>
<p id="phone-error">Incluye el código de país.</p>

No marques aria-invalid="true" antes de que la persona tenga oportunidad de completar el campo. Cuando aparezca el error, muestra texto visible y asócialo; no dependas solo del atributo.

aria-errormessage relaciona un mensaje de error específico, pero aria-describedby tiene compatibilidad amplia y también puede conectar instrucciones. Prueba la combinación elegida con los lectores de pantalla soportados.

Una región en vivo permite anunciar contenido que cambia sin mover el foco.

TécnicaUso
role="status"Resultado o confirmación no urgente; equivale normalmente a un anuncio polite
role="alert"Mensaje urgente que requiere atención inmediata
aria-live="polite"Espera una pausa razonable antes de anunciar
aria-live="assertive"Interrumpe; reservar para situaciones críticas
aria-atomic="true"Anuncia la región completa, no solo el nodo modificado
aria-relevantDefine qué tipos de cambios importan; rara vez necesita cambiarse
aria-busy="true"Indica que la actualización todavía no terminó
<p
id="save-status"
role="status"
aria-atomic="true"
></p>
saveStatus.textContent = "Cambios guardados"

Mantén el contenedor en el DOM antes de insertar el mensaje. Si creas la región y su texto al mismo tiempo, algunas combinaciones de navegador y lector pueden no anunciarlo. No envuelvas toda la aplicación en aria-live: produciría anuncios extensos y repetidos.

role="alert" no significa “texto rojo”. Úsalo para un fallo urgente que acaba de aparecer, no para contenido que ya estaba en la página al cargar ni para cada tecla inválida.

Ocultar contenido del árbol de accesibilidad

Sección titulada «Ocultar contenido del árbol de accesibilidad»
<svg
aria-hidden="true"
focusable="false"
>
<!-- decorativo -->
</svg>

aria-hidden="true" oculta el elemento y todos sus descendientes a tecnologías asistivas, pero puede continuar visible en pantalla. Úsalo para decoración o contenido duplicado que ya tiene una alternativa accesible.

Nunca lo apliques a un elemento enfocable ni a un ancestro que contenga controles enfocables:

<!-- Incorrecto: el botón puede recibir foco, pero desaparece del árbol accesible. -->
<div aria-hidden="true">
<button>Comprar</button>
</div>

Si el contenido debe desaparecer para todas las personas, usa hidden, display: none o visibility: hidden según el caso. Si además necesitas bloquear temporalmente interacción y foco de una zona visible u oculta durante un modal, evalúa el atributo HTML inert.

role="none" y role="presentation" eliminan la semántica del contenedor, pero no necesariamente su texto ni la semántica independiente de todos sus descendientes. No son equivalentes a aria-hidden.

AtributoPropósitoPrecaución
aria-controlsRelaciona control y contenido controladoEl id debe existir; no implementa comportamiento
aria-ownsCambia la relación lógica cuando el DOM no puede representarlaTambién altera el orden accesible; evitar salvo necesidad real
aria-activedescendantMantiene foco en el contenedor e indica el descendiente activoRequiere teclado, scroll y estilos sincronizados
aria-orientationIndica orientación no predeterminadaSolo en roles compatibles
aria-posinset / aria-setsizePosición y tamaño total de una colección parcialÚtil en listas virtualizadas
aria-multiselectableEl widget permite seleccionar varias opcionesSolo en roles compatibles como listbox o grid

No copies estos atributos de un ejemplo aislado. Implementa el patrón completo de la guía APG y confirma que la visualización, el foco y el árbol accesible expresan el mismo estado.

<div
role="tablist"
aria-label="Configuración de cuenta"
>
<button
type="button"
role="tab"
id="profile-tab"
aria-selected="true"
aria-controls="profile-panel"
tabindex="0"
>
Perfil
</button>
<button
type="button"
role="tab"
id="security-tab"
aria-selected="false"
aria-controls="security-panel"
tabindex="-1"
>
Seguridad
</button>
</div>
<section
role="tabpanel"
id="profile-panel"
aria-labelledby="profile-tab"
>
<!-- contenido de Perfil -->
</section>
<section
role="tabpanel"
id="security-panel"
aria-labelledby="security-tab"
hidden
>
<!-- contenido de Seguridad -->
</section>

Los atributos no completan el componente. El código también debe gestionar flechas entre pestañas, Home/End cuando corresponda, un solo tabindex="0", selección, panel visible y foco perceptible. Para una navegación normal entre páginas, usa enlaces y aria-current; no conviertas la navegación en pestañas.

Los IDs deben ser estables y únicos. useId() ayuda a relacionar etiqueta, ayuda y error entre varias instancias:

import { useId } from "react"
function TextField({ label, help }: { label: string; help: string }) {
const inputId = useId()
const helpId = `${inputId}-help`
return (
<div>
<label htmlFor={inputId}>{label}</label>
<input
id={inputId}
aria-describedby={helpId}
/>
<p id={helpId}>{help}</p>
</div>
)
}

Un componente debe exponer las props necesarias para nombre, descripción y estado, pero no aceptar combinaciones imposibles sin advertencia. Por ejemplo, un IconButton debería exigir aria-label si no recibe texto visible.

En HTML, los valores ARIA se serializan como texto. No dejes un atributo booleano con el significado contrario por una conversión incorrecta:

<button aria-expanded={isOpen}>Filtros</button>

React genera "true" o "false". El estado visual y el contenido deben derivarse de la misma fuente isOpen.

  • Agregar aria-label a todos los elementos, incluso cuando ya tienen texto.
  • Repetir roles nativos sin necesidad: <nav role="navigation">.
  • Usar role="button" sin Enter, Space, foco ni estado deshabilitado.
  • Ocultar con aria-hidden un contenedor que conserva enlaces o botones enfocables.
  • Mantener aria-expanded="false" después de abrir visualmente el panel.
  • Usar aria-selected en un botón conmutador donde corresponde aria-pressed.
  • Crear un menú ARIA para una lista común de enlaces.
  • Confiar en aria-disabled para impedir la ejecución.
  • Referenciar IDs inexistentes o duplicados.
  • Usar aria-live="assertive" para notificaciones de baja importancia.
  • Cambiar el nombre accesible sin actualizar el texto visible o la traducción.
  • Suponer que Lighthouse o axe pueden validar por completo el flujo de teclado.
  • ¿Existe un elemento HTML nativo que evite el rol personalizado?
  • ¿Cada control tiene un nombre breve, único y coherente con el texto visible?
  • ¿La descripción complementa el nombre en vez de reemplazarlo?
  • ¿Los IDs de labelledby, describedby y controls existen y son únicos?
  • ¿Los estados ARIA cambian al mismo tiempo que la interfaz visual?
  • ¿El componente funciona con teclado según su patrón?
  • ¿No existe foco dentro de contenido oculto?
  • ¿Los mensajes dinámicos se anuncian una sola vez y con la prioridad correcta?
  • ¿Se probó nombre, rol, estado y orden en el árbol de accesibilidad?
  • ¿Se recorrió el flujo con teclado y al menos un lector de pantalla?