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.