Accesibilidad en el frontend
La accesibilidad es parte de cada cambio de interfaz: se diseña, construye y prueba con ella. Una tarea de interfaz no está terminada hasta que funciona solo con teclado, cada control anuncia nombre, función y estado, las comprobaciones automatizadas pasan y las pruebas consultan por función.
Objetivo: WCAG 2.2 nivel AA — en la UE, la Ley Europea de Accesibilidad (leyes nacionales como BFSG, EN 301 549) la hace obligatoria para muchos productos. Un hallazgo cita un criterio WCAG A/AA o una tarea concreta que un usuario de tecnología de asistencia no puede completar. Los criterios AAA y las convenciones (un <h1>, objetivos de 44 px) son recomendaciones, nunca bloqueantes.
| Tarea | Leer |
|---|---|
| Elección de elementos, puntos de referencia, encabezados, ARIA, nombres accesibles, estados deshabilitados | references/semantics-and-aria.md |
| Anillos de foco, orden de tabulación, enlace de salto, diálogos, cambios de ruta, mapas de teclas de widgets | references/focus-and-keyboard.md |
| Etiquetas, autocompletado, tipos de entrada, errores de validación, comportamiento de envío | references/forms.md |
| Anunciar cambios, regiones activas, notificaciones, texto alternativo, SVG, medios | references/screen-readers.md |
| Contraste, color, tamaños de objetivo, movimiento, zoom, reajuste, manejo de contenido | references/visual-and-motion.md |
| Pruebas automatizadas y manuales, CI | references/testing.md |
| Auditoría/revisión de interfaz existente, formato de informe | references/audit.md |
| Consultar un criterio WCAG 2.2 | references/wcag.md |
Reglas fundamentales — siempre
Priorizar lo nativo. <button> para acciones, <a href> / <Link> del enrutador para navegación, <label>, <fieldset>, <table>, <dialog>, puntos de referencia. Nunca `». Ningún ARIA es mejor que un ARIA incorrecto; una función promete el modelo completo de teclado. Los widgets complejos (combobox, menú, selector de fecha) provienen de la biblioteca headless accesible del proyecto, no se construyen a mano.
Cada elemento interactivo tiene un nombre accesible [4.1.2]. Los botones de solo icono obtienen aria-label, el icono aria-hidden="true". El texto de la etiqueta visible forma parte del nombre accesible [2.5.3]. Los enlaces indican adónde llevan — nada de "haz clic aquí" [2.4.4].
Teclado [2.1.1, 2.1.2]. Todo lo que funciona con un puntero funciona con el teclado; sin trampas. El orden de tabulación = orden visual; nunca un tabIndex positivo. Escape cierra la superposición superior. Las operaciones de arrastrar y gestos tienen una alternativa de puntero único [2.5.7, 2.5.1].
Foco visible [2.4.7, 1.4.11]. Estilo :focus-visible; nunca outline: none sin un reemplazo verificado (≥ 3:1 contra cada color adyacente). El foco no se oculta detrás de encabezados/banners fijos [2.4.11] — scroll-padding-top.
Gestión del foco. Diálogos: el foco se mueve hacia adentro al abrir, se atrapa, el fondo es inerte, vuelve al desencadenante al cerrar. Cambio de ruta en SPA: actualizar document.title [2.4.2] y mover el foco al nuevo <h1> (tabIndex={-1}) o <main>.
Formularios [1.3.1, 3.3.1, 3.3.2]. <label> visible para cada control — un marcador de posición no es una etiqueta. type, inputMode y autoComplete correctos [1.3.5]. Errores en línea, vinculados con aria-describedby, campo aria-invalid="true", enfocar el primer campo no válido al enviar. Validar al enviar — no deshabilitar el botón de envío hasta que sea válido. Nunca bloquear el pegado [3.3.8].
Anunciar cambios dinámicos [4.1.3]. Específico del campo → aria-describedby; no urgente (guardado, recuento de resultados, carga) → role="status"; error urgente a nivel de formulario → role="alert". La región activa existe en el DOM antes de que su texto cambie.
Color y contraste [1.4.3, 1.4.11, 1.4.1]. Texto ≥ 4.5:1 (grande ≥ 3:1), límites de interfaz, iconos y anillos de foco ≥ 3:1 — aplicados mediante tokens de diseño. El color nunca es la única señal.
Objetivos [2.5.8]. ≥ 24×24 píxeles CSS o espacio suficiente; apuntar a 40 px en escritorio / 44 px en táctil. Las superposiciones decorativas reciben pointer-events: none.
Movimiento y tiempo [2.3.3, 2.2.2]. Animaciones opt-in mediante @media (prefers-reduced-motion: no-preference); parallax/reproducción automática desactivada en modo de movimiento reducido. Cualquier cosa que se mueva > 5 s se puede pausar. Las notificaciones con acciones o errores permanecen hasta que se descartan.
Zoom y reajuste [1.4.4, 1.4.10]. Funciona al 200 % de tamaño de texto y 320 píxeles de ancho CSS sin desplazamiento horizontal. Nunca maximum-scale=1 / user-scalable=no.
Conceptos básicos de la página. <html lang> establecido y actualizado con el idioma de la interfaz [3.1.1]; un <main> visible; los encabezados forman un esquema; enlace de salto primero cuando la navegación precede a <main> [2.4.1].
Imágenes. Significativa → alt describe el propósito; decorativa → alt=""; funcional (la imagen es el enlace/botón) → la acción [1.1.1].
Deber de pruebas
Cada cambio de interfaz se publica con:
- pruebas de componentes que consultan por función y nombre (
getByRole) y cubren rutas de teclado (user.tab(),{Escape},toHaveFocus()), - historias de Storybook cuya comprobación addon-a11y se ejecuta como error (falla CI),
- análisis de Playwright +
@axe-core/playwright(etiquetas WCAG 2.x A/AA) en páginas y estados críticos, - una comprobación manual solo con teclado del flujo; una comprobación con lector de pantalla (VoiceOver/NVDA) para flujos críticos.
Las herramientas automatizadas encuentran solo una parte de los problemas — una ejecución limpia de axe no es conformidad con WCAG. Detalles: references/testing.md.
Antes de finalizar — revisar el cambio en busca de
| Patrón | Corrección |
|---|---|
<div onClick> / <span onClick> | <button>, o <a href> cuando navega |
role="button"/role="tab" sin manejo de teclas | elemento nativo o mapa de teclas APG |
outline: none / outline-none sin reemplazo focus-visible | mantener el anillo del navegador o un anillo personalizado verificado |
tabIndex positivo | corregir el orden del DOM, usar 0 / -1 |
<img sin alt | alt="" o una descripción del propósito |
| botón de icono sin nombre | aria-label, icono aria-hidden |
entrada sin <label> / marcador de posición como etiqueta | etiqueta visible vinculada |
disabled={!isValid} en el envío | mantener habilitado, validar al enviar |
{msg && <div role="status">…} | renderizar la región vacía primero, actualizar su texto |
aria-live="assertive" en mensajes de éxito | role="status" |
aria-hidden en o sobre un elemento enfocable | eliminarlo o hacer que el subárbol sea inert |
onPaste + preventDefault | permitir pegar |
maximum-scale=1 / user-scalable=no | eliminar |
encabezado fijo sin scroll-padding-top | rellenar el desplazador |
| arrastrar/gesto sin alternativa de botón | agregar una ruta de puntero único |
transition: all, movimiento sin protección de movimiento reducido | enumerar propiedades, envolver en no-preference |
Informes
Las auditorías y revisiones se informan en el idioma de la conversación, un hallazgo por causa raíz con todas las ubicaciones, criterio WCAG, impacto en el usuario y corrección concreta — formato en references/audit.md. La severidad se asigna a la habilidad de revisión de código: ALTA → Bloqueante/Importante, MEDIA → Importante/Menor, BAJA → Menor.
Fuentes
Combinado de: web-quality-skills de Addy Osmani, accessibility (MIT), better-accessibility de Jakub Krehel (MIT), fixing-accessibility de ibelick (MIT), Web Interface Guidelines de Vercel (MIT); W3C WCAG 2.2 y WAI-ARIA Authoring Practices.