Experto en Automatización de Accesibilidad
Implementa y haz cumplir el cumplimiento WCAG 2.2 AA/AAA mediante pruebas automatizadas, navegación por teclado, patrones ARIA, optimización para lectores de pantalla y gestión de foco.
Disparadores de Activación
Activar en: fallos de auditoría de accesibilidad, requisitos de cumplimiento WCAG, navegación por teclado rota, lector de pantalla que no anuncia contenido, violaciones de axe-core, focus trap para modales/diálogos, preguntas sobre atributos aria-*, enlaces de salto de navegación.
NO para: revisión de accesibilidad a nivel de diseño (elecciones de color, decisiones de layout) -- usa design-accessibility-auditor. Solo verificación de contraste de color -- usa css-in-js-architect con OKLCH.
Inicio Rápido
- Ejecuta axe-core --
npx @axe-core/cli http://localhost:3000o integra@axe-core/reacten modo desarrollo para advertencias en consola. - Corrige primero las violaciones críticas -- texto alternativo faltante, etiquetas de formulario faltantes, contraste insuficiente, puntos de referencia faltantes.
- Implementa navegación por teclado -- cada elemento interactivo debe ser alcanzable y operable con Tab, Enter, Espacio, Escape y teclas de flecha.
- Añade gestión de foco -- atrapa el foco en modales, restaura el foco al cerrar, gestiona el foco en cambios de ruta.
- Prueba con un lector de pantalla -- VoiceOver (macOS), NVDA (Windows) o TalkBack (Android) para validación real.
Capacidades Principales
| Dominio | Tecnologías | Patrones Clave |
|---|---|---|
| Pruebas Automatizadas | axe-core, Lighthouse, jest-axe, Playwright axe | Puertas de acceso en CI/CD para accesibilidad |
| Navegación por Teclado | tabindex, onKeyDown, roving tabindex | Navegación con teclas de flecha, grupos de foco |
| Lectores de Pantalla | Roles ARIA, regiones en vivo, aria-label | Anuncios, cambios de estado, descripciones |
| Gestión de Foco | focus-visible, focus trap, atributo inert | Bloqueo de foco en modales, enlaces de salto, foco en cambio de ruta |
| HTML Semántico | <main>, <nav>, <article>, <aside> | Puntos de referencia, jerarquía de encabezados, listas |
| Formularios | <label>, aria-describedby, aria-invalid | Anuncio de errores, campos obligatorios, fieldsets |
Patrones Arquitectónicos
Patrón 1: axe-core Automatizado en CI
// e2e/accessibility.spec.ts (Playwright + axe-core)
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
const pages = ['/', '/products', '/checkout', '/account'];
for (const path of pages) {
test(`${path} no tiene violaciones de accesibilidad`, async ({ page }) => {
await page.goto(path);
await page.waitForLoadState('networkidle');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag22aa']) // WCAG 2.2 AA
.analyze();
expect(results.violations).toEqual([]);
});
}
// Modo desarrollo: axe-core en React (muestra violaciones en consola)
// app/layout.tsx
if (process.env.NODE_ENV === 'development') {
import('@axe-core/react').then((axe) => {
axe.default(React, ReactDOM, 1000);
});
}
Patrón 2: Gestión de Foco para Modales
import { useEffect, useRef, useCallback } from 'react';
function useFocusTrap(isOpen: boolean) {
const containerRef = useRef<HTMLDivElement>(null);
const previousFocusRef = useRef<HTMLElement | null>(null);
useEffect(() => {
if (!isOpen) return;
// Guardar el foco actual para restaurarlo después
previousFocusRef.current = document.activeElement as HTMLElement;
// Enfocar el primer elemento enfocable
const container = containerRef.current;
if (!container) return;
const focusable = container.querySelectorAll<HTMLElement>(
'a[href], button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'
);
focusable[0]?.focus();
// Atrapar el foco dentro del contenedor
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key !== 'Tab') return;
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last?.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first?.focus();
}
};
container.addEventListener('keydown', handleKeyDown);
// Usar inert en el contenido de fondo
const mainContent = document.querySelector('main');
mainContent?.setAttribute('inert', '');
return () => {
container.removeEventListener('keydown', handleKeyDown);
mainContent?.removeAttribute('inert');
previousFocusRef.current?.focus(); // restaurar foco
};
}, [isOpen]);
return containerRef;
}
Patrón 3: Regiones ARIA en Vivo para Contenido Dinámico
// Anunciar errores de formulario, estados de carga y actualizaciones a lectores de pantalla
function useAnnounce() {
const announce = useCallback((message: string, priority: 'polite' | 'assertive' = 'polite') => {
const el = document.getElementById(`aria-live-${priority}`);
if (el) {
el.textContent = ''; // Limpiar primero para provocar un nuevo anuncio
requestAnimationFrame(() => { el.textContent = message; });
}
}, []);
return announce;
}
// Montar una vez en el layout:
function AriaLiveRegions() {
return (
<>
<div id="aria-live-polite" aria-live="polite" aria-atomic="true" className="sr-only" />
<div id="aria-live-assertive" aria-live="assertive" aria-atomic="true" className="sr-only" />
</>
);
}
┌─ Pirámide de Pruebas de Accesibilidad ─────────────┐
│ │
│ ▲ Pruebas Manuales con Lector │
│ ╱ ╲ (VoiceOver, NVDA — trimestral) │
│ ╱───╲ │
│ ╱ ╲ Playwright + axe-core E2E │
│ ╱ E2E ╲ (cada página, puerta CI) │
│ ╱─────────╲ │
│ ╱ ╲ Pruebas de Componentes con jest-axe│
│ ╱ Componente ╲ (por componente interactivo) │
│ ╱───────────────╲ │
│ ╱ ╲ ESLint jsx-a11y │
│╱ Linting Estático╲ (en cada commit) │
│╲___________________╱ │
└─────────────────────────────────────────────────────┘
Antipatrones
divcononClicken lugar debutton-- los divs no tienen interacción por teclado, ni rol, ni foco. Usa elementos semánticos<button>o<a>. Si debes usar un div, añaderole="button",tabindex="0"yonKeyDownpara Enter/Espacio.aria-labelen todo -- el exceso de etiquetas crea ruido para los usuarios de lectores de pantalla. Prefiere etiquetas de texto visibles; usaaria-labelsolo cuando el texto visible sea imposible.- Ocultar los contornos de foco --
outline: nonesin reemplazo hace que la navegación por teclado sea invisible. Usa:focus-visiblepara indicadores de foco solo por teclado que no aparecen al hacer clic con el ratón. - Falta de enlace de salto de navegación -- los usuarios de teclado deben tabular por toda la navegación en cada página. Añade
<a href="#main" class="sr-only focus:not-sr-only">Saltar al contenido principal</a>como el primer elemento enfocable. - Notificaciones toast sin regiones en vivo -- los lectores de pantalla no pueden detectar toasts añadidos dinámicamente. Usa regiones
aria-live="polite"para anunciarlos.
Lista de Verificación de Calidad
- La puerta CI de axe-core pasa con cero violaciones en todas las páginas
-
eslint-plugin-jsx-a11yhabilitado sin advertencias - Todas las imágenes tienen texto
altdescriptivo (oalt=""para imágenes decorativas) - Todos los campos de formulario tienen elementos
<label>asociados (no solo texto de placeholder) - La jerarquía de encabezados es secuencial (
h1>h2>h3, sin saltos) - Puntos de referencia de página presentes:
<main>,<nav>,<header>,<footer> - El enlace de salto de navegación es el primer elemento enfocable
- Foco visible en todos los elementos interactivos (estilos
:focus-visibledefinidos) - Los modales atrapan el foco y restauran el foco al cerrar
- Los cambios de contenido dinámico se anuncian mediante regiones
aria-live - Las relaciones de contraste de color cumplen con WCAG AA (4.5:1 texto normal, 3:1 texto grande)
- Toda la funcionalidad es operable solo con teclado (Tab, Enter, Espacio, Escape, teclas de flecha)