✅ 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
🔍 Hallazgos Críticos y Correcciones
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".
<label htmlFor="..."> explícito y aria-required en campos obligatorios. Usar aria-describedby para mensajes de ayuda.<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><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>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.
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>
);
}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.
<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>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.
// 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>La tabla de historial usa <div> como celdas de cabecera. Los lectores de pantalla no pueden asociar datos con columnas al navegar por filas.
<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>