ARIA
Enriquece la semántica de componentes complejos para hacerlos accesibles
¿Qué es ARIA?
ARIA (Accessible Rich Internet Applications) es una especificación del W3C que permite mejorar la accesibilidad de aplicaciones web ricas y componentes dinámicos.
🏷️ Roles
Definen qué es un elemento (button, navigation, alert...)
role="button"📊 Estados
Describen el estado actual de un elemento (checked, expanded, disabled...)
aria-expanded="true"🔧 Propiedades
Proporcionan información adicional (label, describedby, controls...)
aria-label="Menú principal"⚠️ Regla de Oro de ARIA
No uses ARIA si puedes usar HTML nativo
Formularios con atributos ARIA
Mal ejemplo
Formulario sin ARIA
❌ Formulario inaccesible sin atributos ARIA apropiados:
<!-- Malo: sin etiqueta, sin descripción -->
<input type="text" placeholder="Tu correo electrónico">
<!-- Malo: icono no accesible -->
<button>
<span class="icon">🔍</span>
</button>
<!-- Malo: error no asociado, sin autocompletado -->
<input type="password">
<div class="error">Contraseña demasiado corta</div>Buen ejemplo
Formulario con ARIA
✅ Formulario accesible con atributos ARIA apropiados:
<!-- Bueno: aria-label para campo sin etiqueta visible -->
<input
type="text"
aria-label="Dirección de correo electrónico (obligatorio)"
aria-describedby="email-desc"
aria-required="true">
<span id="email-desc">Formato: nombre@ejemplo.com</span>
<!-- Bueno: botón con aria-label -->
<button aria-label="Enviar formulario">
<span aria-hidden="true">🔍</span>
</button>
<!-- Bueno: error asociado con aria-describedby -->
<input
type="password"
aria-label="Contraseña (8 caracteres mínimo)"
aria-invalid="true"
aria-describedby="pwd-error"
autocomplete="new-password">
<div id="pwd-error" role="alert">
La contraseña debe contener al menos 8 caracteres
</div>Atributos ARIA usados:
aria-label: Proporciona una etiqueta accesiblearia-describedby: Asocia una descripción detalladaaria-required: Indica un campo obligatorioaria-invalid: Señala un campo inválidoaria-labelledby: Referencia una etiqueta existenterole="alert": Anuncia errores inmediatamentearia-hidden: Oculta elementos decorativosaria-live: Anuncia cambios dinámicos
Explicación: Los atributos ARIA enriquecen los formularios proporcionando etiquetas, descripciones y estados a las tecnologías de asistencia.
Acordeón accesible
Mal ejemplo
Sin atributos ARIA
<!-- Malo: Sin atributos ARIA, sin navegación con teclado -->
<div class="accordion-header" @click="toggle">
<span>Title</span>
<span>+</span>
</div>
<div v-if="open">
Content
</div>Buen ejemplo
Con ARIA apropiado
<!-- Bueno: ARIA completo con navegación con teclado -->
<button
id="accordion-button-1"
aria-expanded="false"
aria-controls="panel-1"
@click="toggle"
@keydown.enter="toggle"
>
Title
</button>
<div
id="panel-1"
role="region"
aria-labelledby="accordion-button-1"
:hidden="!open"
>
Content
</div>Explicación: Un acordeón debe usar atributos ARIA apropiados, soportar navegación con teclado y anunciar cambios de estado.
Tabindex y navegación con teclado
El atributo tabindex controla si un elemento puede recibir enfoque del teclado y en qué orden.
✅ tabindex="0"
El elemento es enfocable en el orden natural del DOM
tabindex="0"<!-- Bueno: Elemento interactivo enfocable -->
<div role="button" aria-label="Custom action" tabindex="0">
Custom button
</div>⚠️ tabindex="-1"
Enfocable por JavaScript pero no por Tab
tabindex="-1"<!-- Bueno: Para enfoque programático -->
<div role="alert" aria-live="assertive" tabindex="-1">
Error message
</div>❌ tabindex > 0
Cambia el orden de navegación (¡evitar!)
tabindex="5"<!-- Malo: Rompe el orden de tabulación natural -->
<button tabindex="3">Third</button>
<button tabindex="1">First</button>
<button tabindex="2">Second</button>📋 Mejores prácticas de Tabindex
- Usa
tabindex="0"para hacer enfocables elementos interactivos personalizados - Usa
tabindex="-1"para enfoque programático (modales, alertas) - NUNCA uses valores positivos - respeta el orden del DOM
- Los elementos interactivos nativos (
<button>,<a>,<input>) no necesitan tabindex
🎯 Ejemplos interactivos
Prueba la navegación con teclado (Tab/Shift+Tab) con estos elementos:
❌ Elementos no enfocables
Párrafo
💡 Consejos de prueba
- Usa Tab para navegar hacia adelante
- Usa Shift + Tab para navegar hacia atrás
- Usa Espacio o Enter para activar elementos
- Observa el indicador de enfoque (contorno azul)
- Nota qué elementos pueden o no pueden recibir enfoque
Menú desplegable accesible
Mal ejemplo
Menú sin ARIA
<!-- Malo: Sin rol de menú, sin trampa de enfoque -->
<div @click="open = !open">
Actions ▼
</div>
<div v-if="open">
<div @click="action">Edit</div>
<div @click="action">Delete</div>
</div>Buen ejemplo
Menú con ARIA
<!-- Bueno: Atributos ARIA y gestión de enfoque -->
<button
aria-haspopup="true"
aria-expanded="false"
aria-controls="menu"
@keydown.escape="close"
>
Actions
</button>
<div
id="menu"
role="menu"
v-if="open"
>
<button role="menuitem">Edit</button>
<button role="menuitem">Delete</button>
</div>Explicación: Un menú desplegable debe soportar navegación con teclado, cerrarse con Escape y mantener el enfoque apropiadamente.
Galería de imágenes accesible
Mal ejemplo
Galería de imágenes accesible
<!-- ❌ Malo: galería no accesible con teclado -->
<div class="thumbnails">
<img v-for="image in images"
:src="image.thumb"
@click="selectImage(index)"
:class="{ active: selected === index }" />
</div>
<div class="main-image">
<img :src="images[selected].full" />
</div>Buen ejemplo
Galería de imágenes accesible
Vista frontal del MacBook Pro de 16 pulgadas con la pantalla cerrada, mostrando el elegante diseño de aluminio.
<!-- ✅ Bueno: galería accesible con navegación con teclado -->
<div role="tablist" aria-label="Imágenes del producto">
<button v-for="(image, index) in images"
:id="`tab-${index}`"
role="tab"
:aria-selected="selected === index"
:aria-controls="`panel-${index}`"
:tabindex="selected === index ? 0 : -1"
@click="selectImage(index)"
@keydown="handleKeydown($event, index)">
<img :src="image.thumb" :alt="`${image.alt} - Miniature`" />
</button>
</div>
<div :id="`panel-${selected}`"
role="tabpanel"
:aria-labelledby="`tab-${selected}`">
<img :src="images[selected].full" :alt="images[selected].alt" />
<p>{{ images[selected].description }}</p>
</div>
<!-- Bonnes pratiques :
- Pattern tablist/tab/tabpanel ARIA
- Navigation avec flèches, Home, End
- Focus management avec tabindex
- Descriptions détaillées pour chaque image -->Explicación: Una galería de imágenes debe permitir navegación con teclado y proporcionar descripciones contextuales para cada imagen. El patrón ARIA tablist/tab/tabpanel es perfecto para este caso de uso.
Tabla ordenable accesible
Mal ejemplo
| Nombre ↑ | Correo electrónico | Rol | Estado |
|---|---|---|---|
| Alice Martin | alice@example.com | Admin | Activo |
| Bob Dupont | bob@example.com | Usuario | Inactivo |
| Charlie Leroy | charlie@example.com | Moderador | Activo |
| Diana Moreau | diana@example.com | Usuario | Suspendido |
Buen ejemplo
| Alice Martin | alice@example.com | Admin | Activo |
|---|---|---|---|
| Bob Dupont | bob@example.com | Usuario | Inactivo |
| Charlie Leroy | charlie@example.com | Moderador | Activo |
| Diana Moreau | diana@example.com | Usuario | Suspendido |
Explicación: Una tabla ordenable debe indicar el estado de ordenación actual, permitir navegación con teclado y anunciar cambios a los lectores de pantalla.
Regiones dinámicas aria-live
Mal ejemplo
Sin aria-live
Los cambios no se anuncian
<!-- Mauvais : pas d'attribut aria-live -->
<div class="status-banner">
<p></p>
</div>Contador: 0
Problemas:
- Los cambios no se anuncian
- Los usuarios de lectores de pantalla no saben que el contenido ha cambiado
- Sin indicación de la importancia del mensaje
Buen ejemplo
Con aria-live apropiado
Los cambios se anuncian según su prioridad
<!-- Bon : avec aria-live="polite" pour informations -->
<div
class="status-banner"
aria-live="polite"
aria-atomic="true"
role="status"
>
<p>Information : Données mises à jour</p>
</div>
<!-- Bon : avec aria-live="assertive" pour alertes -->
<div
class="alert-banner"
aria-live="assertive"
aria-atomic="true"
role="alert"
>
<p>Attention : Action requise immédiatement</p>
</div>Zona polite (información)
Zona assertive (alertas)
Ventajas:
- Los mensajes importantes interrumpen con aria-live="assertive"
- Los mensajes informativos esperan con aria-live="polite"
- aria-atomic="true" anuncia el contenido completo de la región
- Los roles apropiados (status, alert) proporcionan más contexto
Explicación: Las regiones aria-live permiten anunciar cambios de contenido a los lectores de pantalla. 'polite' espera a que termine el anuncio actual, 'assertive' interrumpe inmediatamente.
Botones de iconos accesibles
Mal ejemplo
Buen ejemplo
Explicación: Los botones que contienen solo iconos deben tener un aria-label o texto visualmente oculto para ser entendidos por los lectores de pantalla.
Combobox / Búsqueda con sugerencias
Mal ejemplo
Sin ARIA ni navegación con teclado
Buen ejemplo
Con ARIA y navegación con teclado
Explicación: Una búsqueda con sugerencias (combobox) debe ser navegable con teclado, anunciar resultados y permitir la selección.
Notificaciones toast (Live Regions)
Mal ejemplo
Sin aria-live
❌ Las notificaciones no se anuncian a los lectores de pantalla
<!-- Malo: no anunciado a lectores de pantalla -->
<div v-if="showToast" class="toast">
Votre demande a été enregistrée
</div>Buen ejemplo
Con aria-live apropiado
✅ Las notificaciones se anuncian correctamente
<!-- Bueno: notificación de información (polite) -->
<div
v-if="showToast"
role="status"
aria-live="polite"
aria-atomic="true"
class="toast"
>
<p>Votre demande a été enregistrée</p>
<button @click="dismiss" aria-label="Fermer notification">✕</button>
</div>
<!-- Bueno: alerta de error (assertive) -->
<div
v-if="showError"
role="alert"
aria-live="assertive"
aria-atomic="true"
class="toast error"
>
<p>Erreur : action impossible à exécuter</p>
<button @click="dismiss" aria-label="Fermer alerte">✕</button>
</div>Diferencias:
role="status"+aria-live="polite": Para notificaciones informativas, no interrumpe la lectura actualrole="alert"+aria-live="assertive": Para errores críticos, interrumpe inmediatamentearia-atomic="true": Anuncia el contenido completo de la notificación
Explicación: Las notificaciones dinámicas deben ser anunciadas a los lectores de pantalla sin robar el enfoque. Usa role='status' para información y role='alert' para errores.