♿ ESTÁNDAR AVANZADO DE ACCESIBILIDAD WEB (WCAG 2.1 AA)
🎯 OBJETIVO
Garantizar que todas las interfaces del sistema sean totalmente operables por usuarios que dependen de lectores de pantalla, navegación por teclado o configuraciones de accesibilidad.
🎯 REGLAS INQUEBRANTABLES
[REQUIRED] A11Y-001: Todo elemento interactivo DEBE ser accesible por teclado (Tecla Tab, Enter, Space). Queda prohibido asignar eventos onClick en divs o spans sin role="button" y tabIndex={0}.
Por qué: un
onClicken undivno recibe foco de teclado ni se activa con Enter o Espacio: para quien no usa mouse, ese elemento simplemente no existe. Es la misma clase de bug que un enlace roto, solo que invisible para quien prueba con mouse.
[REQUIRED] A11Y-002: Gestión Estricta de Foco en Modales (Focus Trap). Cuando un modal se abre, el foco debe capturarse dentro de él y restaurarse al elemento detonador al cerrarse.
Por qué: sin captura de foco, tabular dentro de un modal abierto saca el foco hacia elementos de la página que quedaron detrás, invisibles pero seleccionables — confuso con mouse y directamente inutilizable con teclado o lector de pantalla.
[REQUIRED] A11Y-003: Contraste de Color >= 4.5:1 para texto normal. Verificación automatizada en CI/CD con @axe-core/playwright.
Por qué: por debajo de 4.5:1 el texto se vuelve difícil de leer con baja visión o en pantallas con reflejo de luz, y es el umbral objetivo de WCAG AA, no una preferencia estética. Verificarlo en CI es lo que impide que un ajuste de paleta lo rompa sin que nadie lo note.
⌨️ 1. GESTIÓN DE FOCO Y FOCUS TRAPPING EN MODALES
// src/components/Modal.tsx
import { useEffect, useRef } from 'react'
export function Modal({ isOpen, onClose, children }: { isOpen: boolean; onClose: () => void; children: React.ReactNode }) {
const modalRef = useRef<HTMLDivElement>(null)
const previousFocusRef = useRef<HTMLElement | null>(null)
useEffect(() => {
if (isOpen) {
previousFocusRef.current = document.activeElement as HTMLElement
modalRef.current?.focus()
} else {
previousFocusRef.current?.focus() // Restaurar foco
}
}, [isOpen])
if (!isOpen) return null
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div
ref={modalRef}
tabIndex={-1}
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
className="bg-background p-6 rounded-lg max-w-md w-full focus:outline-none"
>
{children}
<button
onClick={onClose}
className="mt-4 px-4 py-2 bg-surface border rounded-lg text-sm"
>
Cerrar
</button>
</div>
</div>
)
}🧪 2. TESTING AUTOMATIZADO CON PLAYWRIGHT Y AXE-CORE
// e2e/accessibility.spec.ts
import { test, expect } from '@playwright/test'
import AxeBuilder from '@axe-core/playwright'
test('Verificar que el Dashboard cumple con WCAG 2.1 AA', async ({ page }) => {
await page.goto('/dashboard')
const accessibilityScanResults = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.analyze()
expect(accessibilityScanResults.violations).toEqual([])
})📋 CHECKLIST DE ACCESIBILIDAD
- [ ] Jerarquía de encabezados única por página (
<h1>principal, luego<h2>,<h3>). - [ ] Atributos
altdescriptivos en todas las imágenes. - [ ] Navegación completa por teclado con indicador de foco visible (
focus-visible). - [ ] Captura de foco (Focus Trap) en diálogos y modales.
- [ ] Pruebas automatizadas con
@axe-core/playwrightsin violaciones WCAG 2.1 AA.