Capítulo 14 — Accesibilidad web¶
Introducción¶
La accesibilidad web consiste en diseñar y construir sitios que cualquier persona pueda usar, independientemente de sus capacidades físicas, sensoriales o cognitivas, del dispositivo o del contexto.
No es un extra opcional. Es una obligación legal en muchos países (WCAG 2.2, ADA, EN 301 549) y, sobre todo, es lo correcto.
Aproximadamente 1 de cada 6 personas tiene algún tipo de discapacidad. Diseñar sin accesibilidad es excluir a millones de usuarios.
Los cuatro principios WCAG: POUR¶
Las Web Content Accessibility Guidelines (WCAG) se organizan en cuatro principios:
| Principio | Significado |
|---|---|
| Perceivable | La información debe poder percibirse (texto alternativo, subtítulos) |
| Operable | Debe poder usarse con teclado, ratón, voz, etc. |
| Understandable | Debe ser comprensible (lenguaje claro, errores descriptivos) |
| Robust | Debe funcionar en distintos navegadores y ayudas técnicas |
Cada principio se divide en criterios con tres niveles: A, AA y AAA. El nivel AA es el estándar exigido por ley en la mayoría de países.
HTML semántico: la base¶
El mayor error de accesibilidad es usar <div> para todo. Un <button> no es solo un botón visual; es un elemento que los lectores de pantalla reconocen, que se activa con Enter y Espacio, y que recibe foco automáticamente.
<!-- ❌ Mal -->
<div onclick="enviar()">Enviar</div>
<!-- ✅ Bien -->
<button type="button" onclick="enviar()">Enviar</button>
Etiquetas semánticas esenciales:
<button>para acciones.<a href="...">para navegación (no<a onclick>).<nav>,<main>,<header>,<footer>,<aside>.<h1>a<h6>en orden jerárquico, sin saltarse niveles.<label>asociado a cada<input>.
Textos alternativos¶
Toda imagen informativa necesita alt.
<!-- Informativa -->
<img src="grafico-ventas.png" alt="Ventas de 2025: 45% en Q1, 30% en Q2, 25% en Q3">
<!-- Decorativa (sin alt, con role) -->
<img src="adorno.png" alt="" role="presentation">
<!-- Icono con enlace -->
<a href="/inicio">
<img src="casa.svg" alt="Ir a la página de inicio">
</a>
Regla práctica
Pregúntate: si no pudiera ver la imagen, ¿qué información perdería? Eso es lo que va en el alt. Si no pierdes nada, alt="".
Contraste de color¶
El texto debe tener contraste suficiente con el fondo.
- AA: contraste mínimo 4.5:1 en texto normal, 3:1 en texto grande.
- AAA: 7:1 y 4.5:1 respectivamente.
Herramientas:
- WebAIM Contrast Checker
- Colour Contrast Analyser
- DevTools de Chrome (pestaña "Accessibility").
Nunca uses solo el color para transmitir información.
<!-- ❌ Mal: solo cambia el color -->
<p style="color: red;">Error en el formulario</p>
<!-- ✅ Bien: color + icono + texto -->
<p style="color: #b91c1c;">
<span aria-hidden="true">⚠</span>
<strong>Error:</strong> el email no es válido.
</p>
Navegación por teclado¶
Todo debe poder usarse sin ratón. Prueba tu web pulsando Tab repetidamente:
- ¿Ves dónde está el foco?
- ¿Puedes llegar a todos los enlaces y botones?
- ¿Puedes activarlos con
EnteroEspacio? - ¿Puedes cerrar los modales con
Esc?
:focus-visible¶
Nunca elimines el contorno del foco sin reemplazarlo:
/* ❌ Mal */
button:focus { outline: none; }
/* ✅ Bien */
button:focus-visible {
outline: 3px solid #6a5acd;
outline-offset: 2px;
}
Orden de tabulación¶
El orden por defecto sigue el DOM. Si necesitas modificarlo, usa tabindex:
tabindex="0": añade el elemento al orden natural.tabindex="-1": focuseable por script, pero no por teclado.tabindex="1+": evítalo, rompe el orden natural.
ARIA: cuándo y cómo usarlo¶
ARIA (Accessible Rich Internet Applications) añade semántica cuando HTML no la tiene. Su primera regla es:
Regla nº1 de ARIA
No uses ARIA si puedes usar HTML nativo. Un <button> con ARIA sigue siendo peor que un <button> nativo.
Atributos más usados:
| Atributo | Función |
|---|---|
role |
Define el tipo de elemento (role="dialog", role="alert") |
aria-label |
Nombre accesible cuando no hay texto visible |
aria-labelledby |
Referencia a otro elemento que da el nombre |
aria-describedby |
Referencia a otro elemento con descripción |
aria-hidden="true" |
Oculta de lectores de pantalla |
aria-live |
Anuncia cambios dinámicos |
aria-expanded |
Estado de un menú/acordeón |
Ejemplo: botón con solo icono¶
<button aria-label="Cerrar menú">
<svg>...</svg>
</button>
Ejemplo: alerta dinámica¶
<div role="alert" aria-live="polite">
Tu perfil se ha guardado correctamente.
</div>
Ejemplo: acordeón¶
<button aria-expanded="false" aria-controls="seccion1">
¿Qué es HTML?
</button>
<div id="seccion1" hidden>
HTML es el lenguaje de marcado...
</div>
Formularios accesibles¶
Cada input debe tener un <label> asociado:
<label for="email">Correo electrónico</label>
<input type="email" id="email" name="email" required>
Errores claros y asociados¶
<label for="email">Correo</label>
<input
type="email"
id="email"
aria-invalid="true"
aria-describedby="email-error"
>
<p id="email-error" role="alert">El correo no tiene un formato válido.</p>
Agrupaciones¶
<fieldset>
<legend>Método de pago</legend>
<label><input type="radio" name="pago" value="tarjeta"> Tarjeta</label>
<label><input type="radio" name="pago" value="paypal"> PayPal</label>
</fieldset>
Skip links¶
Un enlace al inicio del body para saltar directamente al contenido:
<a href="#main" class="skip-link">Saltar al contenido principal</a>
...
<main id="main">...</main>
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: #6a5acd;
color: white;
padding: 8px 16px;
z-index: 100;
}
.skip-link:focus {
top: 0;
}
Movimiento reducido¶
Algunas personas sufren mareos con animaciones. Respétalo:
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
Herramientas para auditar¶
- axe DevTools — extensión de navegador.
- Lighthouse — pestaña Accessibility en DevTools.
- WAVE — análisis online.
- Pa11y — CLI para CI.
- Lectores de pantalla: NVDA (Windows), VoiceOver (macOS/iOS), TalkBack (Android).
Testing manual imprescindible¶
Ninguna herramienta automática detecta todo. Prueba tú:
- Navega solo con teclado. Sin ratón. Todo el flujo.
- Activa el lector de pantalla y navega por la home.
- Pon el zoom al 200%. ¿Sigue siendo usable?
- Usa modo alto contraste del sistema operativo.
- Prueba con
prefers-reduced-motionactivado.
Buenas prácticas¶
- HTML semántico primero, ARIA solo cuando sea imprescindible.
- Todos los
inputcon<label>asociado. - Todos los botones con texto visible o
aria-label. - Contraste AA mínimo siempre.
- Foco visible en todos los elementos interactivos.
- Enlaces con texto descriptivo: "Descargar el informe anual" en lugar de "clic aquí".
langcorrecto en<html lang="es">.- Títulos jerárquicos sin saltos (
h1→h2→h3). - No bloquees el zoom con
user-scalable=no.
Ejercicios¶
- Audita tu portfolio con Lighthouse y corrige los problemas detectados.
- Navega toda la home solo con
Tab. Anota qué no funciona bien. - Añade un skip link y verifícalo con el teclado.
- Crea un formulario accesible con errores bien asociados.
- Implementa
prefers-reduced-motionen tus animaciones. - Escribe un
<dialog>accesible (foco atrapado, cierre con Esc, retorno del foco).
⬅️ Capítulo 13 — Módulos y bundlers | ➡️ Capítulo 15 — TypeScript desde cero