Checklist WCAG 2.2 — Estado Actual

Estado Criterio Nombre Nivel Impacto
1.1.1 Contenido no textual
Gráficas sin aria-label ni role="img"
A CRÍTICO
1.3.1 Información y relaciones
Tabla sin <th> ni scope
A CRÍTICO
1.4.3 Contraste (mínimo)
Texto gris claro sobre fondo blanco: 2.8:1
AA CRÍTICO
2.1.1 Teclado
Modal no capturable por teclado, foco se pierde
A CRÍTICO
2.4.1 Omitir bloques
Sin skip link al contenido principal
A ALTO
3.3.2 Etiquetas en formularios
5 inputs sin <label> asociado
A CRÍTICO
⚠️ 2.4.7 Foco visible
Algunos botones secundarios ocultan el outline
AA MEDIO
⚠️ 2.5.8 Tamaño objetivo mínimo
Iconos de acción: 16×16px (mín. 24×24)
AA MEDIO
1.4.4 Cambio de tamaño de texto
Funciona correctamente al 200%
AA OK
2.4.3 Orden de foco lógico
Secuencia de tabulación coherente en la mayoría de vistas
AA OK
1.3.4 Orientación
Funciona en portrait y landscape
AA OK

🎨 Análisis de Contraste de Color

Texto ayuda
2.8:1
Requerido: 4.5:1 (AA)
❌ FALLO — #9CA3AF sobre blanco
Estado activo
4.6:1
Requerido: 4.5:1 (AA)
✅ PASA — #16A34A sobre #F0FDF4
Botón primario
7.1:1
Requerido: 4.5:1 (AA)
✅ PASA (AAA) — excelente
Alerta warning
2.1:1
Requerido: 4.5:1 (AA)
❌ FALLO — cambiar a #78350F
Navbar dark
17.1:1
Requerido: 4.5:1 (AA)
✅ PASA (AAA) — perfecto
Texto secundario
3.9:1
Requerido: 4.5:1 (AA)
❌ FALLO — usar #4B5563 mínimo

🔍 Hallazgos Críticos y Correcciones

Formulario de registro: inputs sin <label> WCAG 3.3.2 A
CRÍTICO

5 campos de entrada del formulario de registro de alimentos carecen de etiquetas <label> asociadas programáticamente. Los lectores de pantalla anuncian los campos como "editar texto vacío".

⚠️
Problema detectado con NVDA + ChromeAl navegar con Tab, el campo de "Cantidad (g)" no anuncia su propósito. NVDA lee "Editar". El usuario no puede completar el formulario sin visión.
Solución aplicadaAñadir <label htmlFor="..."> explícito y aria-required en campos obligatorios. Usar aria-describedby para mensajes de ayuda.
❌ Antes
<div className="field"> {/* Sin label — lector de pantalla: "Editar texto" */} <input type="number" placeholder="Cantidad (g)" className="input" /> <span className="hint">Introduce el peso en gramos</span> </div>
✅ Después
<div className="field"> <label htmlFor="cantidad-gramos" className="label"> Cantidad <span aria-hidden="true" className="required-mark"> *</span> <span className="sr-only">(obligatorio)</span> </label> <input id="cantidad-gramos" type="number" aria-required="true" aria-describedby="cantidad-hint" className="input" /> <p id="cantidad-hint" className="hint"> Introduce el peso en gramos</p> </div>
Modal sin focus trap ni cierre por Escape WCAG 2.1.1 A
CRÍTICO

El modal de "Detalle Nutricional" no atrapa el foco. Al pulsar Tab desde el último elemento del modal, el foco escapa al contenido subyacente. Tampoco cierra con la tecla Escape.

⚠️
Trampa de teclado inversaEl usuario con discapacidad visual puede quedar atrapado fuera del modal sin forma de volver. Violación directa de WCAG 2.1.2.
✅ Patrón correcto
function ModalNutricional({ isOpen, onClose, alimento }) { const titleId = React.useId(); // Cerrar con Escape React.useEffect(() => { const handler = (e) => { if (e.key === "Escape" && isOpen) onClose(); }; document.addEventListener("keydown", handler); return () => document.removeEventListener("keydown", handler); }, [isOpen, onClose]); if (!isOpen) return null; return ( <div role="dialog" aria-modal="true" aria-labelledby={titleId} > {/* Backdrop inerte para AT */} <div aria-hidden="true" onClick={onClose} /> {/* FocusTrap garantiza Tab dentro del modal */} <FocusTrap> <div className="modal-panel"> <h2 id={titleId}>{alimento.nombre}</h2> {/* contenido */} <button onClick={onClose} aria-label="Cerrar modal"></button> </div> </FocusTrap> </div> ); }
Gráficas de barras sin alternativa textual WCAG 1.1.1 A
CRÍTICO

Las 4 gráficas del dashboard (calorías diarias, macros, hidratación, tendencia semanal) son SVG sin role, aria-label ni tabla de datos alternativa.

✅ Solución: gráfica accesible
<figure> {/* SVG con rol semántico y descripción */} <svg role="img" aria-labelledby="chart-title" aria-describedby="chart-desc" viewBox="0 0 400 200" > <title id="chart-title">Calorías diarias — semana 23/06</title> <desc id="chart-desc"> Gráfica de barras: lun 1820kcal, mar 2100kcal, mié 1950kcal, jue 2200kcal, vie 1780kcal. Promedio semanal: 1970 kcal (objetivo: 2000). </desc> {/* barras SVG */} </svg> {/* Tabla oculta visualmente para lectores de pantalla */} <table className="sr-only"> <caption>Calorías diarias</caption> <thead><tr><th scope="col">Día</th> <th scope="col">kcal</th></tr></thead> <tbody>...filas de datos...</tbody> </table> </figure>
Menú lateral sin skip link WCAG 2.4.1 A
ALTO

No existe mecanismo para saltar el menú de navegación lateral (32 ítems) al llegar con teclado. El usuario debe tabular por todos los ítems antes de llegar al contenido principal.

✅ Skip link + ARIA landmark
// En el layout raíz — primer elemento del DOM <a href="#main-content" className={cn( "sr-only focus:not-sr-only", "focus:fixed focus:top-4 focus:left-4 focus:z-50", "focus:bg-white focus:px-4 focus:py-2 focus:rounded-md", "focus:ring-2 focus:ring-blue-600 focus:shadow-lg" )} > Saltar al contenido principal </a> <nav aria-label="Navegación principal"> {/* 32 ítems de menú */} </nav> <main id="main-content" tabIndex={-1} {/* permite focus programático */} > {children} </main>
Tabla de historial sin encabezados semánticos WCAG 1.3.1 A
ALTO

La tabla de historial usa <div> como celdas de cabecera. Los lectores de pantalla no pueden asociar datos con columnas al navegar por filas.

✅ Tabla semántica accesible
<table> <caption className="sr-only"> Historial de ingestas — últimos 7 días </caption> <thead> <tr> <th scope="col">Fecha</th> <th scope="col">Alimento</th> <th scope="col">Cantidad</th> <th scope="col">Calorías</th> <th scope="col"> <span className="sr-only">Acciones</span> </th> </tr> </thead> <tbody> {ingestas.map((item) => ( <tr key={item.id}> <td>{item.fecha}</td> <td>{item.alimento}</td> <td>{item.cantidad}g</td> <td>{item.kcal} kcal</td> <td> <button aria-label={`Eliminar ${item.alimento} del ${item.fecha}`} >Eliminar</button> </td> </tr> ))} </tbody> </table>

🗓️ Roadmap de Implementación Priorizado

1
Sprint crítico — Bloqueos legales
Correcciones que impiden el uso con tecnología de asistencia
Semana 1–2
!Añadir <label> a todos los inputs del formulario de registro (3h)
!Implementar focus trap en todos los modales con @headlessui/react (4h)
!Añadir cierre por Escape en overlays y dropdowns (2h)
!Añadir alt/title/desc a todas las gráficas SVG (5h)
2
Correcciones de alto impacto
Mejoras de contraste y semántica HTML
Semana 3
Actualizar paleta: grises de ayuda a #4B5563 mínimo (2h)
Reemplazar <div> de tabla por <th scope="col"> + <caption> (3h)
Implementar skip link en layout raíz (1h)
Añadir aria-label a nav, main, aside (1h)
3
Mejoras de UX accesible
Focus visible, tamaños objetivo, live regions
Semana 4
Asegurar focus ring visible en todos los botones (outline: 2px) (2h)
Aumentar iconos de acción a mínimo 24×24px (1h)
Añadir live region para mensajes de éxito/error en formularios (3h)
Implementar prefers-reduced-motion en animaciones (2h)
4
Verificación y certificación
Pruebas con usuarios reales y documentación
Semana 5
Auditoría final con axe-core (CI integrado) + WAVE manual (4h)
Test con VoiceOver (macOS) y NVDA (Windows) en flujos críticos (6h)
Test con TalkBack en Android 14 (3h)
Declaración de Accesibilidad WCAG 2.2 AA (2h)