Documentación de UXR SEO Analyzer

Introducción

Etiquetas ARIA

Ver contenido

Introducción

Las etiquetas ARIA proporcionan nombres accesibles para elementos que los lectores de pantalla anuncian a los usuarios. Cuando el HTML nativo no proporciona un nombre claro—como botones solo con íconos o widgets personalizados—los atributos ARIA llenan el vacío. WCAG 4.1.2 requiere que todos los componentes de interfaz de usuario tengan nombres determinables programáticamente, haciendo el etiquetado ARIA esencial para la accesibilidad.

Sin nombres accesibles apropiados, los usuarios de lectores de pantalla escuchan anuncios sin sentido como “botón” o “enlace” sin entender qué hace el elemento. Las etiquetas ARIA aseguran que cada elemento interactivo comunique su propósito claramente.

Entendiendo los Nombres Accesibles

¿Qué Es un Nombre Accesible?

Un nombre accesible es el texto que las tecnologías asistivas anuncian cuando un usuario enfoca un elemento. Puede venir de varias fuentes:

Fuente Prioridad Ejemplo
aria-labelledby Más alta Referencia texto de otro elemento
aria-label Alta Texto de etiqueta directo
Elemento <label> Media Etiquetas de campos de formulario
Contenido del elemento Baja Texto dentro de botones/enlaces
Atributo title Más baja Opción de último recurso

El Cálculo del Nombre

Los navegadores usan un algoritmo específico para determinar nombres accesibles:

<!-- 1. aria-labelledby gana (prioridad más alta) -->
<button aria-labelledby="btn-label">
  <svg>...</svg>
</button>
<span id="btn-label">Buscar productos</span>
<!-- Anunciado: "Buscar productos, botón" -->

<!-- 2. aria-label es siguiente -->
<button aria-label="Buscar productos">
  <svg>...</svg>
</button>
<!-- Anunciado: "Buscar productos, botón" -->

<!-- 3. Contenido tiene prioridad menor -->
<button>
  <svg aria-hidden="true">...</svg>
  Buscar
</button>
<!-- Anunciado: "Buscar, botón" -->

Atributos de Etiquetado ARIA

aria-label

Proporciona una cadena directamente como nombre accesible:

<!-- Botón solo con ícono -->
<button aria-label="Cerrar diálogo">
  <svg aria-hidden="true"><!-- ícono X --></svg>
</button>

<!-- Input de búsqueda -->
<input type="search" aria-label="Buscar productos">

Mejor para:

  • Botones solo con íconos
  • Inputs sin etiquetas visibles
  • Elementos donde el contexto visual es claro

Limitaciones:

  • No traducible por herramientas de traducción del navegador
  • No visible para usuarios videntes

aria-labelledby

Referencia el texto de otro elemento como etiqueta:

<!-- Usando encabezado existente -->
<h2 id="section-title">Carrito de Compras</h2>
<nav aria-labelledby="section-title">
  <!-- Navegación dentro del carrito -->
</nav>

<!-- Múltiples elementos -->
<span id="label">Correo</span>
<span id="hint">(requerido)</span>
<input type="email" aria-labelledby="label hint">
<!-- Anunciado: "Correo (requerido)" -->

Mejor para:

  • Cuando el texto de la etiqueta ya existe visualmente
  • Combinar múltiples fuentes de texto
  • Secciones etiquetadas por encabezados

aria-describedby

Proporciona descripción suplementaria (no el nombre principal):

<label for="password">Contraseña</label>
<input type="password" id="password" aria-describedby="pwd-help">
<p id="pwd-help">Debe tener al menos 8 caracteres con un número</p>
<!-- Anunciado: "Contraseña, editar texto, Debe tener al menos 8 caracteres..." -->

Diferencia con labelledby:

  • aria-labelledby = nombre principal (anunciado primero)
  • aria-describedby = descripción adicional (anunciada después)

Casos de Uso Comunes de Etiquetas ARIA

Botones con Íconos

<!-- Íconos de redes sociales -->
<a href="https://twitter.com/company" aria-label="Síguenos en Twitter">
  <svg aria-hidden="true"><!-- ícono Twitter --></svg>
</a>

<button aria-label="Agregar a favoritos">
  <svg aria-hidden="true"><!-- ícono corazón --></svg>
</button>

Landmarks de Navegación

<!-- Múltiples elementos nav necesitan distinción -->
<nav aria-label="Navegación principal">
  <!-- Navegación primaria -->
</nav>

<nav aria-label="Navegación de pie de página">
  <!-- Enlaces del footer -->
</nav>

<nav aria-label="Migas de pan">
  <!-- Ruta de breadcrumb -->
</nav>

Regiones de Formulario

<section aria-labelledby="shipping-title">
  <h2 id="shipping-title">Información de Envío</h2>
  <!-- Campos de formulario de envío -->
</section>

<section aria-labelledby="billing-title">
  <h2 id="billing-title">Información de Facturación</h2>
  <!-- Campos de formulario de facturación -->
</section>

Widgets Personalizados

<!-- Calificación con estrellas -->
<div role="slider"
     aria-label="Calificación"
     aria-valuemin="1"
     aria-valuemax="5"
     aria-valuenow="4">
  <!-- Íconos de estrellas -->
</div>

<!-- Interruptor toggle -->
<button role="switch"
        aria-checked="false"
        aria-label="Modo oscuro">
  <!-- Visual del toggle -->
</button>

Errores Comunes de Etiquetado

1. Etiquetas Faltantes

<!-- MAL: Sin nombre accesible -->
<button>
  <svg><!-- ícono --></svg>
</button>
<!-- Anunciado: "botón" - ¡sin sentido! -->

<!-- BIEN: Tiene nombre accesible -->
<button aria-label="Eliminar elemento">
  <svg aria-hidden="true"><!-- ícono --></svg>
</button>

2. Etiquetas Redundantes

<!-- MAL: Información redundante -->
<button aria-label="Botón enviar">Enviar</button>
<!-- Anunciado: "Botón enviar, botón" - "botón" se dice dos veces -->

<!-- BIEN: Dejar que el contenido sea el nombre -->
<button>Enviar</button>
<!-- Anunciado: "Enviar, botón" -->

3. Etiquetas Que No Coinciden con Texto Visible

<!-- MAL: Diferente del texto visible -->
<button aria-label="Enviar mensaje">Enviar Formulario</button>
<!-- Usuarios de control por voz dicen "click Enviar Formulario" pero no funcionará -->

<!-- BIEN: Coincidir o incluir texto visible -->
<button aria-label="Enviar Formulario - envía tu mensaje">Enviar Formulario</button>

4. Usar aria-label en Elementos No Interactivos

<!-- MAL: aria-label en div (lectores de pantalla pueden ignorar) -->
<div aria-label="Sección importante">Contenido aquí</div>

<!-- BIEN: Usar en landmarks o elementos interactivos -->
<section aria-label="Sección importante">Contenido aquí</section>

Probando Etiquetas ARIA

Prueba Manual Rápida

  1. Abrir DevTools del navegador
  2. Inspeccionar un elemento
  3. Verificar el panel de Accesibilidad
  4. Buscar propiedad “Name”
  5. Verificar que tenga sentido

Usando Lectores de Pantalla

Lector de Pantalla Comando
NVDA Tab al elemento, escuchar
VoiceOver VO+Flecha derecha
JAWS Tab al elemento
Narrator Tab al elemento

Herramientas Automatizadas

  • axe DevTools - Reporta nombres accesibles faltantes
  • WAVE - Resalta elementos sin etiquetar
  • Lighthouse - Verifica nombres accesibles
  • DevTools del navegador - Muestra nombre accesible calculado

Resumen de Mejores Prácticas

Hacer No Hacer
Usar aria-label para elementos solo con íconos Poner aria-label en divs no interactivos
Usar aria-labelledby cuando existe texto Duplicar texto visible en aria-label
Hacer etiquetas concisas pero claras Escribir etiquetas demasiado largas
Empezar con verbo de acción para botones Incluir tipo de elemento en la etiqueta
Probar con lectores de pantalla reales Asumir que claridad visual = accesibilidad

Cuándo Usar Cada Atributo

Situación Usar
Botón solo con ícono aria-label
Etiqueta visual existente cerca aria-labelledby
Campo de formulario sin etiqueta visible aria-label
Texto de ayuda adicional aria-describedby
Múltiples landmarks de navegación aria-label en cada uno
Sección con encabezado aria-labelledby al encabezado

Implementación

El Algoritmo de Cálculo del Nombre Accesible

Orden de Prioridad

Los navegadores siguen un algoritmo específico (accname-1.2) para determinar nombres accesibles:

1. aria-labelledby (prioridad más alta)
2. aria-label
3. Etiquetado nativo (<label>, <caption>, <legend>)
4. Contenido del elemento (nodos de texto, texto alt)
5. Atributo title (prioridad más baja, evitar)

Entendiendo la Precedencia

<!-- aria-labelledby sobrescribe todo -->
<button aria-labelledby="ext-label" aria-label="Ignorado">
  También ignorado
</button>
<span id="ext-label">Esto se anuncia</span>
<!-- Anunciado: "Esto se anuncia, botón" -->

<!-- aria-label sobrescribe contenido -->
<button aria-label="Esto se anuncia">
  Esto se ignora
</button>
<!-- Anunciado: "Esto se anuncia, botón" -->

<!-- El contenido se usa cuando no hay ARIA -->
<button>Esto se anuncia</button>
<!-- Anunciado: "Esto se anuncia, botón" -->

Nombre desde el Contenido

Algunos roles permiten nombre desde el contenido (texto dentro del elemento):

<!-- Roles que obtienen nombre del contenido -->
<button>Enviar Formulario</button>        <!-- ✅ Funciona -->
<a href="#">Saber Más</a>                  <!-- ✅ Funciona -->
<td>Valor de celda</td>                    <!-- ✅ Funciona -->
<h1>Título de Página</h1>                  <!-- ✅ Funciona -->

<!-- Roles que NO obtienen nombre del contenido -->
<div role="img">Descripción aquí</div>    <!-- ❌ Necesita aria-label -->
<nav>Navegación</nav>                      <!-- ❌ Texto ignorado para nombre -->
<section>Texto de sección</section>        <!-- ❌ Texto ignorado para nombre -->

Implementando aria-label

Uso Básico

<!-- Botones con íconos -->
<button aria-label="Cerrar" class="close-btn">
  <svg aria-hidden="true">
    <use href="#icon-close"></use>
  </svg>
</button>

<!-- Búsqueda sin etiqueta visible -->
<div class="search-container">
  <input type="search" aria-label="Buscar productos">
  <button aria-label="Enviar búsqueda">
    <svg aria-hidden="true"><!-- ícono buscar --></svg>
  </button>
</div>

<!-- Landmarks de navegación -->
<nav aria-label="Principal">...</nav>
<nav aria-label="Pie de página">...</nav>
<nav aria-label="Migas de pan">...</nav>

aria-label Dinámico

// Ejemplo React
function ToggleButton({ isActive, label }) {
  return (
    <button
      aria-label={`${isActive ? 'Desactivar' : 'Activar'} ${label}`}
      aria-pressed={isActive}
      onClick={toggle}
    >
      <Icon name={isActive ? 'check' : 'circle'} />
    </button>
  );
}

// Ejemplo Vue 3
<template>
  <button
    :aria-label="`${isActive ? 'Desactivar' : 'Activar'} ${label}`"
    :aria-pressed="isActive"
    @click="toggle"
  >
    <Icon :name="isActive ? 'check' : 'circle'" />
  </button>
</template>

Cuándo NO Usar aria-label

<!-- NO: En elementos que ya tienen texto visible -->
<button aria-label="Enviar">Enviar</button>
<!-- Redundante - solo usa el contenido -->

<!-- NO: Diferente del texto visible -->
<button aria-label="Enviar mensaje">Enviar</button>
<!-- Confunde a usuarios de control por voz -->

<!-- NO: En elementos no interactivos -->
<p aria-label="Párrafo importante">Texto aquí</p>
<!-- Lectores de pantalla pueden ignorar -->

<!-- NO: Para agregar descripciones largas -->
<button aria-label="Haz click en este botón para enviar el formulario y recibirás un correo de confirmación dentro de 24 horas">
  Enviar
</button>
<!-- Usa aria-describedby para descripciones largas -->

Implementando aria-labelledby

Referenciando Texto Existente

<!-- Referencia única -->
<h2 id="cart-heading">Carrito de Compras (3 artículos)</h2>
<section aria-labelledby="cart-heading">
  <!-- Contenido del carrito -->
</section>

<!-- Múltiples referencias (separadas por espacio) -->
<span id="fname-label">Nombre</span>
<span id="fname-required">(requerido)</span>
<span id="fname-format">Solo letras</span>
<input
  type="text"
  aria-labelledby="fname-label fname-required"
  aria-describedby="fname-format"
>
<!-- Nombre: "Nombre (requerido)" -->
<!-- Descripción: "Solo letras" -->

Patrón de Auto-Referencia

<!-- Incluir el contenido propio del elemento en la etiqueta -->
<button
  id="buy-btn"
  aria-labelledby="buy-btn product-name"
>
  Comprar
</button>
<span id="product-name">Mouse Inalámbrico</span>
<!-- Anunciado: "Comprar Mouse Inalámbrico, botón" -->

Etiquetado de Diálogos

<div
  role="dialog"
  aria-labelledby="dialog-title"
  aria-describedby="dialog-desc"
>
  <h2 id="dialog-title">Confirmar Eliminación</h2>
  <p id="dialog-desc">
    ¿Estás seguro de que quieres eliminar este elemento?
    Esta acción no se puede deshacer.
  </p>
  <button>Cancelar</button>
  <button>Eliminar</button>
</div>

Etiquetado de Tablas y Grillas

<!-- Tabla con caption -->
<table aria-labelledby="table-caption">
  <caption id="table-caption">Informe de Ventas Trimestral 2024</caption>
  <thead>...</thead>
  <tbody>...</tbody>
</table>

<!-- Grilla con encabezado externo -->
<h2 id="grid-title">Comparación de Productos</h2>
<div role="grid" aria-labelledby="grid-title">
  <div role="row">
    <div role="columnheader">Característica</div>
    <div role="columnheader">Básico</div>
    <div role="columnheader">Pro</div>
  </div>
  <!-- filas -->
</div>

Implementando aria-describedby

Texto de Ayuda en Campos de Formulario

<div class="form-group">
  <label for="username">Nombre de Usuario</label>
  <input
    type="text"
    id="username"
    aria-describedby="username-help username-error"
  >
  <p id="username-help" class="help-text">
    3-20 caracteres, solo letras y números
  </p>
  <p id="username-error" class="error" hidden>
    El nombre de usuario ya está en uso
  </p>
</div>
// Mostrar error cuando falla la validación
function showError(input, errorElement) {
  errorElement.hidden = false;
  input.setAttribute('aria-invalid', 'true');
  // aria-describedby ya incluye el ID del error
}

Descripciones de Widgets Complejos

<!-- Slider con descripción -->
<div class="slider-container">
  <label id="volume-label">Volumen</label>
  <div
    role="slider"
    aria-labelledby="volume-label"
    aria-describedby="volume-desc"
    aria-valuemin="0"
    aria-valuemax="100"
    aria-valuenow="50"
    tabindex="0"
  >
    <!-- track y thumb del slider -->
  </div>
  <p id="volume-desc">
    Usa las teclas de flecha para ajustar. El valor actual se muestra como porcentaje.
  </p>
</div>

Patrones Específicos por Componente

Botones con Íconos Accesibles

<!-- Botón simple con ícono -->
<button aria-label="Configuración" class="icon-btn">
  <svg aria-hidden="true" focusable="false">
    <use href="#icon-settings"></use>
  </svg>
</button>

<!-- Botón con ícono y badge -->
<button aria-label="Notificaciones, 5 sin leer" class="icon-btn">
  <svg aria-hidden="true" focusable="false">
    <use href="#icon-bell"></use>
  </svg>
  <span class="badge" aria-hidden="true">5</span>
</button>

<!-- Botón toggle con ícono -->
<button
  aria-label="Marcar favorito"
  aria-pressed="false"
  class="icon-btn"
>
  <svg aria-hidden="true" focusable="false">
    <use href="#icon-bookmark"></use>
  </svg>
</button>

Cards Accesibles

<article class="card" aria-labelledby="card-1-title">
  <img src="product.jpg" alt="">
  <h3 id="card-1-title">Auriculares Inalámbricos</h3>
  <p>Audio de alta calidad con 30 horas de batería.</p>
  <p class="price">$149.99</p>
  <a
    href="/productos/auriculares-inalambricos"
    aria-label="Ver detalles de Auriculares Inalámbricos"
  >
    Ver Detalles
  </a>
</article>

Tabs Accesibles

<div class="tabs-container">
  <div role="tablist" aria-label="Configuración de Cuenta">
    <button
      role="tab"
      id="tab-profile"
      aria-selected="true"
      aria-controls="panel-profile"
    >
      Perfil
    </button>
    <button
      role="tab"
      id="tab-security"
      aria-selected="false"
      aria-controls="panel-security"
      tabindex="-1"
    >
      Seguridad
    </button>
    <button
      role="tab"
      id="tab-notifications"
      aria-selected="false"
      aria-controls="panel-notifications"
      tabindex="-1"
    >
      Notificaciones
    </button>
  </div>

  <div
    role="tabpanel"
    id="panel-profile"
    aria-labelledby="tab-profile"
  >
    <!-- Contenido del perfil -->
  </div>
</div>

Acordeón Accesible

<div class="accordion">
  <h3>
    <button
      aria-expanded="true"
      aria-controls="section1-content"
      id="section1-header"
    >
      Información de Envío
    </button>
  </h3>
  <div
    id="section1-content"
    role="region"
    aria-labelledby="section1-header"
  >
    <p>Enviamos a todo el mundo con envío gratis en pedidos mayores a $50.</p>
  </div>

  <h3>
    <button
      aria-expanded="false"
      aria-controls="section2-content"
      id="section2-header"
    >
      Política de Devolución
    </button>
  </h3>
  <div
    id="section2-content"
    role="region"
    aria-labelledby="section2-header"
    hidden
  >
    <p>Se aceptan devoluciones dentro de 30 días de la compra.</p>
  </div>
</div>

Integración con Frameworks

Patrones React

// Componente de botón accesible
interface IconButtonProps {
  icon: string;
  label: string;
  onClick: () => void;
  badge?: number;
}

function IconButton({ icon, label, onClick, badge }: IconButtonProps) {
  const computedLabel = badge
    ? `${label}, ${badge} ${badge === 1 ? 'elemento' : 'elementos'}`
    : label;

  return (
    <button
      aria-label={computedLabel}
      onClick={onClick}
      className="icon-btn"
    >
      <Icon name={icon} aria-hidden="true" />
      {badge && (
        <span className="badge" aria-hidden="true">{badge}</span>
      )}
    </button>
  );
}

// Uso
<IconButton
  icon="cart"
  label="Carrito de compras"
  badge={3}
  onClick={openCart}
/>

Patrones Vue 3

<!-- AccessibleDialog.vue -->
<script setup lang="ts">
import { ref, computed } from 'vue'

const props = defineProps<{
  title: string
  description?: string
}>()

const titleId = computed(() => `dialog-title-${crypto.randomUUID()}`)
const descId = computed(() => `dialog-desc-${crypto.randomUUID()}`)
</script>

<template>
  <div
    role="dialog"
    aria-modal="true"
    :aria-labelledby="titleId"
    :aria-describedby="description ? descId : undefined"
  >
    <h2 :id="titleId">{{ title }}</h2>
    <p v-if="description" :id="descId">{{ description }}</p>
    <slot />
  </div>
</template>

Probando Etiquetas ARIA

Pruebas Automatizadas

// Jest + Testing Library
import { render, screen } from '@testing-library/react';

test('botón con ícono tiene nombre accesible', () => {
  render(<IconButton icon="close" label="Cerrar diálogo" onClick={jest.fn()} />);

  const button = screen.getByRole('button', { name: 'Cerrar diálogo' });
  expect(button).toBeInTheDocument();
});

test('diálogo está correctamente etiquetado', () => {
  render(<Dialog title="Confirmar Acción" description="¿Estás seguro?" />);

  const dialog = screen.getByRole('dialog', { name: 'Confirmar Acción' });
  expect(dialog).toHaveAccessibleDescription('¿Estás seguro?');
});

Pruebas de Accesibilidad con Playwright

// a11y.spec.js
const { test, expect } = require('@playwright/test');
const AxeBuilder = require('@axe-core/playwright').default;

test('todos los botones tienen nombres accesibles', async ({ page }) => {
  await page.goto('/');

  const buttons = await page.locator('button').all();

  for (const button of buttons) {
    const name = await button.getAttribute('aria-label') ||
                 await button.textContent();
    expect(name?.trim()).toBeTruthy();
  }
});

test('sin violaciones axe para etiquetado', async ({ page }) => {
  await page.goto('/');

  const results = await new AxeBuilder({ page })
    .withRules(['button-name', 'link-name', 'image-alt'])
    .analyze();

  expect(results.violations).toHaveLength(0);
});

Errores Comunes y Soluciones

Error 1: Referencias Vacías

<!-- MAL: La referencia no existe -->
<button aria-labelledby="nonexistent">Click</button>
<!-- ¡El nombre está vacío! -->

<!-- BIEN: Asegurar que la referencia existe -->
<span id="btn-label" class="sr-only">Enviar formulario</span>
<button aria-labelledby="btn-label">
  <svg>...</svg>
</button>

Error 2: Uso Excesivo de ARIA

<!-- MAL: ARIA donde no es necesario -->
<button aria-label="Enviar" role="button">Enviar</button>

<!-- BIEN: Elementos nativos ya son accesibles -->
<button>Enviar</button>

Error 3: Etiquetado Inconsistente

<!-- MAL: Misma acción, diferentes etiquetas -->
<button aria-label="Cerrar">X</button>
<button aria-label="Descartar">X</button>
<button aria-label="Salir">X</button>

<!-- BIEN: Etiquetado consistente -->
<button aria-label="Cerrar">X</button>
<button aria-label="Cerrar">X</button>
<button aria-label="Cerrar">X</button>

Checklist de Resumen

Checklist de Implementación

  • [ ] Todos los elementos interactivos tienen nombres accesibles
  • [ ] Botones solo con íconos usan aria-label
  • [ ] Widgets complejos usan aria-labelledby para etiquetas visibles
  • [ ] Texto de ayuda usa aria-describedby
  • [ ] Múltiples landmarks de navegación están diferenciados
  • [ ] Diálogos tienen título y descripción
  • [ ] Sin etiquetado redundante
  • [ ] Etiquetas coinciden con texto visible cuando está presente
  • [ ] Etiquetas dinámicas se actualizan apropiadamente
  • [ ] Probado con lectores de pantalla

Artículos Relacionados


Referencias

  1. W3C - WCAG 2.2 SC 4.1.2 Name, Role, Value
  2. W3C - Accessible Name Computation
  3. WebAIM - ARIA
  4. MDN - Using aria-label
  5. W3C - ARIA Authoring Practices
  6. Deque - Accessible Name Calculation

Artículos relacionados

En la misma categoría

Introducción

Rastreabilidad de IA

A medida que asistentes de IA como ChatGPT, Claude, Gemini y Perplexity se convierten en fuentes primarias de información, surge una nueva pregunta: ¿Deberías...

Guía detallada

Guía de Gestión de Rastreadores IA

Gestionar el acceso de rastreadores de IA requiere entender el diverso panorama de bots de IA, sus propósitos y los mecanismos técnicos para controlarlos

Introducción

Texto alternativo para lectores de pantalla

El texto alternativo (alt text) proporciona una descripción textual de las imágenes para usuarios que no pueden verlas

Introducción

Texto alt para SEO: cómo escribirlo por tipo de imagen

Cada imagen en tu sitio web está ayudando o perjudicando tu SEO

Guía detallada

Optimización de alt text: keywords, automatización y auditoría

La optimización de texto alt se encuentra en la intersección del SEO, la accesibilidad y la experiencia de usuario

Introducción

Headers de Caché HTTP

El caché HTTP permite a los navegadores almacenar copias de recursos localmente, eliminando la necesidad de descargarlos nuevamente en visitas posteriores

Última actualización: