Accesibilidad (a11y)
Directrices de accesibilidad exhaustivas basadas en WCAG 2.2 y auditorías de accesibilidad de Lighthouse. Objetivo: hacer que el contenido sea utilizable por todas las personas, incluyendo aquellas con discapacidades.
Flujo de trabajo de auditoría basada en evidencia
Cuando una página renderizada está disponible:
- Ejecutar una auditoría de accesibilidad de Lighthouse en vivo cuando esa capacidad esté disponible; con Chrome DevTools MCP, usar
lighthouse_audit. Usar el modo de navegación móvil para una página pública general o el modo de instantánea cuando recargar perdería el estado autenticado o creado por el usuario. - Usar los nodos de auditoría fallidos para localizar el componente o plantilla relevante, en lugar de buscar en todo el repositorio patrones genéricos.
- Inspeccionar una instantánea del árbol de accesibilidad renderizada para nombres, roles, estados, puntos de referencia y estructura de encabezados; con Chrome DevTools MCP, usar
take_snapshot. Ejercitar el flujo afectado con el teclado. - Corregir el código fuente, luego volver a ejecutar la misma auditoría e interacción manual.
Si las herramientas en vivo no están disponibles, usar Lighthouse CLI o axe para cobertura automatizada y completar las mismas comprobaciones manuales. Las herramientas automatizadas detectan solo un subconjunto de barreras de accesibilidad: una puntuación de 100 no es conformidad con WCAG, y una puntuación baja no reemplaza la evidencia a nivel de problema.
Principios WCAG: POUR
| Principio | Descripción |
|---|---|
| Perceivable (Perceptible) | El contenido puede ser percibido a través de diferentes sentidos |
| Operable (Operable) | La interfaz puede ser operada por todos los usuarios |
| Understandable (Comprensible) | El contenido y la interfaz son comprensibles |
| Robust (Robusto) | El contenido funciona con tecnologías de asistencia |
Niveles de conformidad
| Nivel | Requisito | Objetivo |
|---|---|---|
| A | Accesibilidad mínima | Debe cumplirse |
| AA | Cumplimiento estándar | Debería cumplirse (requisito legal en muchas jurisdicciones) |
| AAA | Accesibilidad mejorada | Bueno tener |
Perceptible
Alternativas textuales (1.1)
Las imágenes requieren texto alternativo:
<!-- ❌ Falta alt -->
<img src="chart.png">
<!-- ✅ Alt descriptivo -->
<img src="chart.png" alt="Gráfico de barras que muestra un aumento del 40% en las ventas del tercer trimestre">
<!-- ✅ Imagen decorativa (alt vacío) -->
<img src="decorative-border.png" alt="" role="presentation">
<!-- ✅ Imagen compleja con descripción más larga -->
<figure>
<img src="infographic.png" alt="Infografía de tendencias del mercado 2024"
aria-describedby="infographic-desc">
<figcaption id="infographic-desc">
<!-- Descripción detallada -->
</figcaption>
</figure>
Los botones de icono necesitan nombres accesibles:
<!-- ❌ Sin nombre accesible -->
<button><svg><!-- icono de menú --></svg></button>
<!-- ✅ Usando aria-label -->
<button aria-label="Abrir menú">
<svg aria-hidden="true"><!-- icono de menú --></svg>
</button>
<!-- ✅ Usando texto oculto visualmente -->
<button>
<svg aria-hidden="true"><!-- icono de menú --></svg>
<span class="visually-hidden">Abrir menú</span>
</button>
Clase oculta visualmente:
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
Contraste de color (1.4.3, 1.4.6)
| Tamaño de texto | Mínimo AA | Mejorado AAA |
|---|---|---|
| Texto normal (< 18px / < 14px negrita) | 4.5:1 | 7:1 |
| Texto grande (≥ 18px / ≥ 14px negrita) | 3:1 | 4.5:1 |
| Componentes de UI y gráficos | 3:1 | 3:1 |
/* ❌ Bajo contraste (2.5:1) */
.low-contrast {
color: #999;
background: #fff;
}
/* ✅ Contraste suficiente (7:1) */
.high-contrast {
color: #333;
background: #fff;
}
/* ✅ Los estados de foco también necesitan contraste (3:1 contra el fondo, WCAG 1.4.11) */
:focus-visible {
outline: 2px solid currentColor;
outline-offset: 2px;
}
No confiar solo en el color:
<!-- ❌ Solo el color indica error -->
<input class="error-border">
<style>.error-border { border-color: red; }</style>
<!-- ✅ Color + icono + texto -->
<div class="field-error">
<input aria-invalid="true" aria-describedby="email-error">
<span id="email-error" class="error-message">
<svg aria-hidden="true"><!-- icono de error --></svg>
Por favor, introduzca una dirección de correo electrónico válida
</span>
</div>
Alternativas multimedia (1.2)
<!-- Video con subtítulos -->
<video controls>
<source src="video.mp4" type="video/mp4">
<track kind="captions" src="captions.vtt" srclang="en" label="Inglés" default>
<track kind="descriptions" src="descriptions.vtt" srclang="en" label="Descripciones">
</video>
<!-- Audio con transcripción -->
<audio controls>
<source src="podcast.mp3" type="audio/mp3">
</audio>
<details>
<summary>Transcripción</summary>
<p>Texto completo de la transcripción...</p>
</details>
Operable
Accesible por teclado (2.1)
Toda la funcionalidad debe ser accesible por teclado. Preferir elementos interactivos nativos: <button>, <a href> y controles de formulario manejan la activación con Enter/Espacio, el foco y la semántica de tecnologías de asistencia de forma gratuita. Solo agregar manejo manual de teclado cuando no se pueda usar un elemento nativo.
<!-- ❌ Elemento no interactivo con solo clic: no enfocable, sin activación por teclado -->
<div class="card" onclick="handleAction()">Abrir</div>
<!-- ✅ Mejor: usar un botón nativo -->
<button type="button" onclick="handleAction()">Abrir</button>
// ✅ Cuando DEBE usar un elemento no interactivo (por ejemplo, div con role="button"),
// hacerlo enfocable Y manejar la activación por teclado. NO agregar esto a un <button>
// nativo: Enter/Espacio ya activan el clic, por lo que se dispararía dos veces.
element.setAttribute('role', 'button');
element.setAttribute('tabindex', '0');
element.addEventListener('click', handleAction);
element.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleAction();
}
});
Sin trampas de teclado. Los usuarios deben poder Tabular hacia y desde cada componente. Usar el patrón de trampa de foco modal para diálogos: el elemento <dialog> nativo maneja esto automáticamente.
Foco visible (2.4.7)
/* ❌ Nunca eliminar contornos de foco */
*:focus { outline: none; }
/* ✅ Usar :focus-visible para foco solo de teclado */
:focus {
outline: none;
}
:focus-visible {
outline: 2px solid currentColor; /* hereda el color del texto → ya verificado por contraste */
outline-offset: 2px;
}
/* ✅ O elegir un color de marca y verificar contraste ≥3:1 contra cada fondo donde se aplique */
button:focus-visible {
box-shadow: 0 0 0 3px rgba(0, 95, 204, 0.5);
}
Foco no oculto (2.4.11) — nuevo en 2.2
Cuando un elemento recibe foco de teclado, no debe estar completamente oculto por otro contenido creado por el autor, como encabezados fijos, pies de página o paneles superpuestos. En el nivel AAA (2.4.12), ninguna parte del elemento enfocado puede estar oculta.
/* ✅ Tener en cuenta los encabezados fijos al desplazarse a elementos enfocados */
:target {
scroll-margin-top: 80px;
}
/* ✅ Asegurar que los elementos enfocados eviten las barras fijas/pegajosas */
:focus {
scroll-margin-top: 80px;
scroll-margin-bottom: 60px;
}
Enlaces de salto (2.4.1)
Proporcionar un enlace de salto para que los usuarios de teclado puedan omitir la navegación repetitiva. Ver el patrón de enlace de salto para el marcado y estilos completos.
Tamaño del objetivo (2.5.8) — nuevo en 2.2
Los objetivos interactivos deben tener al menos 24 × 24 píxeles CSS (AA). Excepciones: enlaces de texto en línea, elementos donde el navegador controla el tamaño y objetivos donde un círculo de 24px centrado en el cuadro delimitador no se superpone con otro objetivo.
/* ✅ Tamaño mínimo del objetivo */
button,
[role="button"],
input[type="checkbox"] + label,
input[type="radio"] + label {
min-width: 24px;
min-height: 24px;
}
/* ✅ Tamaño cómodo del objetivo (recomendado 44×44) */
.touch-target {
min-width: 44px;
min-height: 44px;
display: inline-flex;
align-items: center;
justify-content: center;
}
Movimientos de arrastre (2.5.7) — nuevo en 2.2
Cualquier acción que requiera arrastrar debe tener una alternativa de puntero único (por ejemplo, botones, entradas). Ver el patrón de movimientos de arrastre para un ejemplo de lista ordenable.
Tiempo (2.2)
// Permitir a los usuarios extender los límites de tiempo
function showSessionWarning() {
const modal = createModal({
title: 'Sesión a punto de expirar',
content: 'Su sesión expirará en 2 minutos.',
actions: [
{ label: 'Extender sesión', action: extendSession },
{ label: 'Cerrar sesión', action: logout }
],
timeout: 120000
});
}
Movimiento (2.3)
/* Respetar la preferencia de movimiento reducido */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
Comprensible
Idioma de la página (3.1.1)
<!-- ❌ Sin idioma especificado -->
<html>
<!-- ✅ Idioma especificado -->
<html lang="es">
<!-- ✅ Cambios de idioma dentro de la página -->
<p>La palabra francesa para hola es <span lang="fr">bonjour</span>.</p>
Navegación consistente (3.2.3)
<!-- La navegación debe ser consistente entre páginas -->
<nav aria-label="Principal">
<ul>
<li><a href="/" aria-current="page">Inicio</a></li>
<li><a href="/productos">Productos</a></li>
<li><a href="/acerca-de">Acerca de</a></li>
</ul>
</nav>
Ayuda consistente (3.2.6) — nuevo en 2.2
Si un mecanismo de ayuda (información de contacto, widget de chat, enlace de preguntas frecuentes, opción de autoayuda) se repite en varias páginas, debe aparecer en el mismo orden relativo cada vez. Los usuarios que confían en una ubicación consistente no deberían tener que buscar ayuda en cada página.
Etiquetas de formulario (3.3.2)
Cada entrada necesita una etiqueta asociada programáticamente. Ver el patrón de etiquetas de formulario para ejemplos explícitos, implícitos e instructivos.
Manejo de errores (3.3.1, 3.3.3)
Anunciar errores a los lectores de pantalla con role="alert" o aria-live, establecer aria-invalid="true" en los campos no válidos y enfocar el primer error al enviar. Ver el patrón de manejo de errores para el marcado y JS completos.
Entrada redundante (3.3.7) — nuevo en 2.2
No obligar a los usuarios a volver a ingresar información que ya proporcionaron en la misma sesión. Autocompletar a partir de pasos anteriores, o permitir a los usuarios seleccionar de valores ingresados anteriormente. Excepciones: reconfirmación de seguridad y contenido que ha caducado.
<!-- ✅ Autocompletar dirección de envío desde la facturación -->
<fieldset>
<legend>Dirección de envío</legend>
<label>
<input type="checkbox" id="same-as-billing" checked>
Igual que la dirección de facturación
</label>
<!-- Campos autocompletados cuando está marcado -->
</fieldset>
Autenticación accesible (3.3.8) — nuevo en 2.2
Los flujos de inicio de sesión no deben depender de pruebas de función cognitiva (por ejemplo, recordar una contraseña, resolver un rompecabezas) a menos que al menos uno de:
- Un mecanismo de copiar y pegar o autocompletar esté disponible
- Exista un método alternativo (por ejemplo, clave de paso, SSO, enlace de correo electrónico)
- La prueba use reconocimiento de objetos o contenido personal (solo AA; AAA elimina esta excepción)
<!-- ✅ Permitir pegar en campos de contraseña -->
<input type="password" id="password" autocomplete="current-password">
<!-- ✅ Ofrecer alternativas sin contraseña -->
<button type="button">Iniciar sesión con clave de paso</button>
<button type="button">Enviarme un enlace de inicio de sesión</button>
Robusto
Uso de ARIA (4.1.2)
Preferir elementos nativos:
<!-- ❌ Rol ARIA en div -->
<div role="button" tabindex="0">Haz clic</div>
<!-- ✅ Botón nativo -->
<button>Haz clic</button>
<!-- ❌ Casilla de verificación ARIA -->
<div role="checkbox" aria-checked="false">Opción</div>
<!-- ✅ Casilla de verificación nativa -->
<label><input type="checkbox"> Opción</label>
Cuando se necesita ARIA, usar los roles y estados correctos. Ver el patrón de pestañas ARIA para un ejemplo completo de lista de pestañas.
Regiones en vivo (4.1.3)
Usar regiones aria-live para anunciar cambios de contenido dinámico sin mover el foco. Ver el patrón de regiones en vivo para el marcado y un helper showNotification().
Lista de verificación de pruebas
Pruebas automatizadas
Preferir una auditoría de Lighthouse en vivo que devuelva los nodos renderizados fallidos directamente al agente. Con Chrome DevTools MCP, esto es lighthouse_audit. De lo contrario:
# Auditoría de accesibilidad de Lighthouse
npx lighthouse https://ejemplo.com --only-categories=accessibility
# axe-core
npm install @axe-core/cli -g
axe https://ejemplo.com
Pruebas manuales
- Navegación por teclado: Tabular por toda la página, usar Enter/Espacio para activar
- Lector de pantalla: Probar con VoiceOver (Mac), NVDA (Windows) o TalkBack (Android)
- Zoom: Contenido utilizable al 200% de zoom
- Alto contraste: Probar con el Modo de alto contraste de Windows
- Movimiento reducido: Probar con
prefers-reduced-motion: reduce - Orden de foco: Lógico y sigue el orden visual
- Tamaño del objetivo: Los elementos interactivos cumplen con el mínimo de 24×24px
Ver la referencia de comandos de lector de pantalla para atajos de VoiceOver y NVDA.
Problemas comunes por impacto
Crítico (corregir inmediatamente)
- Etiquetas de formulario faltantes
- Texto alternativo de imagen faltante
- Contraste de color insuficiente
- Trampas de teclado
- Sin indicadores de foco
Grave (corregir antes del lanzamiento)
- Idioma de página faltante
- Estructura de encabezados faltante
- Texto de enlace no descriptivo
- Multimedia con reproducción automática
- Enlaces de salto faltantes
Moderado (corregir pronto)
- Etiquetas ARIA faltantes en iconos
- Navegación inconsistente
- Identificación de errores faltante
- Tiempo sin controles
- Regiones de referencia faltantes


