La accesibilidad web significa que las personas con discapacidades pueden usar tu sitio. Eso incluye personas que usan lectores de pantalla porque no pueden ver, personas que navegan solo con teclado porque no pueden usar ratón, personas con visión reducida que necesitan alto contraste o texto grande, y personas con discapacidades cognitivas que necesitan una interfaz clara y predecible.
La buena noticia es que la mayor parte de la accesibilidad web no requiere librerías especiales ni técnicas complejas: requiere escribir HTML semántico correcto y seguir unos cuantos patrones bien establecidos. En esta guía aprenderás los fundamentos prácticos que puedes aplicar desde hoy.
Por qué importa la accesibilidad
Alrededor del 15% de la población mundial tiene algún tipo de discapacidad. En España, más de 4 millones de personas tienen alguna discapacidad reconocida. Además, la accesibilidad beneficia a más personas de las que parece: los subtítulos de vídeo también los usan personas en entornos ruidosos; el alto contraste también ayuda a personas mirando una pantalla bajo el sol; la navegación por teclado también la usan programadores que prefieren no usar el ratón.
Desde el punto de vista legal, en muchos países la accesibilidad web es un requisito legal para sitios públicos y empresas por encima de cierto tamaño (en España, la WCAG 2.1 nivel AA es obligatoria para el sector público y empresas grandes). Desde el punto de vista técnico, el HTML accesible tiende a ser mejor HTML en general: más semántico, más fácil de mantener y mejor indexado por los motores de búsqueda.
HTML semántico: la base de todo
El HTML semántico usa las etiquetas correctas para cada tipo de contenido. Los lectores de pantalla dependen de la semántica para entender la estructura de la página y anunciarla al usuario.
<!-- ❌ Sin semántica: div y span para todo -->
<div class="cabecera">
<div class="logo">MiSitio</div>
<div class="nav">
<div class="enlace">Inicio</div>
<div class="enlace">Blog</div>
</div>
</div>
<div class="main">
<div class="articulo">
<div class="titulo">Mi artículo</div>
<div class="texto">...</div>
</div>
</div>
<div class="pie">© 2026</div>
<!-- ✅ Con semántica: cada elemento usa su etiqueta correcta -->
<header>
<a href="/" class="logo">MiSitio</a>
<nav>
<ul>
<li><a href="/">Inicio</a></li>
<li><a href="/blog">Blog</a></li>
</ul>
</nav>
</header>
<main>
<article>
<h1>Mi artículo</h1>
<p>...</p>
</article>
</main>
<footer>© 2026</footer>
Los elementos de estructura más importantes:
<header> — cabecera del sitio o de una sección
<nav> — navegación principal
<main> — contenido principal (solo uno por página)
<article> — contenido independiente (un post, una noticia, una tarjeta)
<section> — sección temática dentro de un artículo o página
<aside> — contenido secundario (barra lateral, relacionados)
<footer> — pie del sitio o de una sección
<figure> — imagen, diagrama o media con su descripción
<figcaption> — descripción de un figure
Jerarquía de encabezados
Los lectores de pantalla permiten a los usuarios navegar por la página saltando de encabezado en encabezado, como si fuera un índice. Por eso la jerarquía debe ser lógica y no saltarse niveles.
<!-- ❌ Jerarquía rota: se usa h3 para el subtítulo sin h2 previo -->
<h1>Guía de accesibilidad</h1>
<h3>Qué es la accesibilidad</h3> <!-- salta de h1 a h3 -->
<h2>HTML semántico</h2>
<!-- ❌ Se usa h2 solo porque se ve más pequeño que h1 -->
<h2 class="titulo-grande">Bienvenido</h2>
<!-- ✅ Jerarquía lógica: h1 → h2 → h3 sin saltos -->
<h1>Guía de accesibilidad</h1>
<h2>Fundamentos</h2>
<h3>Qué es la accesibilidad</h3>
<h3>Por qué importa</h3>
<h2>HTML semántico</h2>
<h3>Elementos de estructura</h3>
<h3>Jerarquía de encabezados</h3>
<!-- Reglas:
- Solo un h1 por página (el título principal)
- No saltar niveles (de h1 a h3 sin h2)
- Usar el nivel correcto según la jerarquía, no según el tamaño visual
- El tamaño visual se controla con CSS -->
Imágenes accesibles
<!-- Toda imagen necesita un atributo alt -->
<!-- ✅ Imagen informativa: el alt describe el contenido -->
<img src="grafico-ventas.png" alt="Gráfico de ventas: el Q3 de 2026 aumentó un 23% respecto al Q2">
<!-- ✅ Imagen decorativa: alt vacío (el lector de pantalla la ignora) -->
<img src="separador-decorativo.png" alt="">
<!-- NUNCA omitas el alt: sin él, el lector anuncia la URL del archivo -->
<!-- ✅ Imagen de logo con enlace: el alt debe describir el destino del enlace -->
<a href="/">
<img src="logo.png" alt="MiSitio — volver a la página de inicio">
</a>
<!-- ✅ Imagen de icono junto a texto: alt vacío porque el texto ya lo describe -->
<button>
<img src="icono-buscar.svg" alt="">
Buscar
</button>
<!-- ✅ Imagen compleja (diagrama, gráfico): longa description -->
<figure>
<img src="diagrama-arquitectura.png"
alt="Diagrama de arquitectura del sistema"
aria-describedby="descripcion-diagrama">
<figcaption id="descripcion-diagrama">
El sistema consta de tres capas: presentación (React),
API (Node.js) y persistencia (PostgreSQL). Las peticiones
pasan por un balanceador de carga antes de llegar a la API.
</figcaption>
</figure>
Formularios accesibles
Los formularios son el área donde más fallos de accesibilidad se concentran. Un formulario inaccesible puede hacer que una persona que usa lector de pantalla no pueda registrarse, pagar o enviar información.
<!-- ❌ Formulario inaccesible: los campos no tienen etiquetas asociadas -->
<p>Nombre</p>
<input type="text" placeholder="Tu nombre">
<p>Email</p>
<input type="email" placeholder="tu@email.com">
<!-- ✅ Cada campo tiene su label asociada -->
<div>
<label for="nombre">Nombre</label>
<input id="nombre" type="text" name="nombre" autocomplete="name">
</div>
<div>
<label for="email">Email</label>
<input id="email" type="email" name="email" autocomplete="email">
</div>
<!-- Formulario completo con buenas prácticas de accesibilidad -->
<form>
<div class="campo">
<label for="nombre">
Nombre completo
<span aria-hidden="true">*</span> <!-- * visible pero el lector no lo lee -->
</label>
<input
id="nombre"
type="text"
name="nombre"
required
aria-required="true"
autocomplete="name"
aria-describedby="nombre-ayuda nombre-error"
>
<p id="nombre-ayuda" class="ayuda">Tal como aparece en tu DNI</p>
<p id="nombre-error" class="error" role="alert" hidden>
El nombre es obligatorio
</p>
</div>
<div class="campo">
<label for="contraseña">Contraseña</label>
<input
id="contraseña"
type="password"
name="contraseña"
required
aria-required="true"
aria-describedby="contraseña-requisitos"
minlength="8"
>
<ul id="contraseña-requisitos">
<li>Mínimo 8 caracteres</li>
<li>Al menos una letra mayúscula</li>
</ul>
</div>
<!-- Nota sobre campos obligatorios -->
<p><span aria-hidden="true">*</span> Campo obligatorio</p>
<button type="submit">Registrarse</button>
</form>
<!-- Mostrar errores de validación de forma accesible -->
<!-- Cuando hay un error, actualizar el campo así: -->
<input
id="email"
type="email"
aria-invalid="true" <!-- indica que el valor es inválido -->
aria-describedby="email-error" <!-- asocia el mensaje de error -->
>
<p id="email-error" role="alert"> <!-- role="alert" lo anuncia inmediatamente -->
Introduce un email válido (ejemplo: tu@mail.com)
</p>
<!-- Los mensajes de error deben:
- Ser específicos (no solo "campo incorrecto")
- Aparecer cerca del campo
- Ser anunciados por el lector de pantalla (role="alert" o aria-live)
- Explicar cómo corregir el error -->
Navegación por teclado
Muchos usuarios navegan con el teclado: personas con discapacidades motoras, usuarios avanzados, personas que usan solo lector de pantalla. Tu interfaz debe ser completamente usable sin ratón.
<!-- ❌ Usar div o span como botón: no es enfocable ni activable por teclado -->
<div class="btn" onclick="hacerAlgo()">Hacer algo</div>
<!-- ✅ Usar el elemento correcto: button es focusable y responde a Enter y Espacio -->
<button onclick="hacerAlgo()">Hacer algo</button>
<!-- ❌ Enlace sin href: no es enfocable por teclado -->
<a onclick="navegar()">Ir a inicio</a>
<!-- ✅ Enlace con href real -->
<a href="/inicio">Ir a inicio</a>
<!-- Si DEBES usar un div como elemento interactivo (casos excepcionales): -->
<div
role="button" <!-- comunicar que es un botón al lector de pantalla -->
tabindex="0" <!-- hacerlo enfocable con Tab -->
onclick="hacerAlgo()"
onkeydown="if(event.key==='Enter'||event.key===' ')hacerAlgo()"
>
Hacer algo
</div>
<!-- Pero en casi todos los casos es mejor usar <button> -->
/* El foco del teclado debe ser visible siempre */
/* ❌ Eliminar el outline sin reemplazarlo */
button:focus { outline: none; } /* hace la navegación por teclado imposible */
/* ✅ Personalizar el foco para que sea visible y encaje con el diseño */
button:focus-visible {
outline: 3px solid #005fcc;
outline-offset: 2px;
border-radius: 4px;
}
/* :focus-visible muestra el foco solo cuando el usuario navega por teclado,
no cuando hace clic con el ratón (mejor UX) */
<!-- Orden de tabulación lógico -->
<!-- El orden de Tab sigue el orden en el DOM, por defecto -->
<!-- Si cambias el orden visual con CSS, asegúrate de que el Tab sigue siendo lógico -->
<!-- tabindex="0": incluir un elemento en el orden natural de Tab -->
<div role="button" tabindex="0">...</div>
<!-- tabindex="-1": enfocable programáticamente pero no con Tab -->
<!-- Útil para: mover el foco a un modal al abrirlo -->
<div id="modal" tabindex="-1">...</div>
<script>
document.getElementById('modal').focus(); // enfoca el modal al abrirlo
</script>
<!-- tabindex positivo (1, 2, 3...): EVITAR -->
<!-- Cambia el orden de tabulación de forma global y crea confusión -->
ARIA: cuando el HTML no es suficiente
ARIA (Accessible Rich Internet Applications) son atributos que añaden información semántica a elementos HTML cuando el HTML por sí solo no puede expresarla. La regla de oro: usa HTML semántico primero; usa ARIA solo cuando el HTML no tenga el elemento correcto.
<!-- aria-label: etiqueta cuando no hay texto visible -->
<button aria-label="Cerrar modal">✕</button>
<button aria-label="Buscar"><svg>...</svg></button>
<nav aria-label="Navegación principal">...</nav>
<nav aria-label="Navegación de pie de página">...</nav>
<!-- aria-label diferencia dos nav en la misma página -->
<!-- aria-labelledby: apuntar a un elemento que actúa como etiqueta -->
<section aria-labelledby="titulo-seccion">
<h2 id="titulo-seccion">Nuestros servicios</h2>
<p>...</p>
</section>
<!-- aria-describedby: apuntar a un elemento que describe -->
<input aria-describedby="contraseña-ayuda">
<p id="contraseña-ayuda">Mínimo 8 caracteres</p>
<!-- aria-expanded: estado de un elemento expandible -->
<button aria-expanded="false" aria-controls="menu">Menú</button>
<ul id="menu" hidden>...</ul>
<!-- Actualizar con JavaScript al abrir/cerrar: -->
<!-- button.setAttribute('aria-expanded', 'true'); -->
<!-- aria-hidden: ocultar al lector de pantalla -->
<span aria-hidden="true">★★★★☆</span>
<span class="sr-only">4 de 5 estrellas</span>
<!-- El icono visual se oculta al lector, el texto descriptivo lo reemplaza -->
<!-- aria-live: anunciar cambios dinámicos -->
<p aria-live="polite">{mensaje de estado}</p>
<!-- polite: anuncia cuando el usuario no está haciendo nada -->
<p role="alert">{error urgente}</p>
<!-- role="alert" = aria-live="assertive": interrumpe e informa inmediatamente -->
Texto solo para lectores de pantalla
A veces necesitas dar contexto a los usuarios de lector de pantalla sin que ese texto sea visible en la interfaz. La clase sr-only (screen reader only) es la solución estándar:
/* CSS: clase estándar para texto solo para lectores de pantalla */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* display:none u opacity:0 también ocultan al lector de pantalla (no sirven aquí) */
/* visibility:hidden también oculta al lector (no sirve) */
/* Solo esta técnica de posicionamiento oculta visualmente pero mantiene el texto accesible */
<!-- Uso de sr-only en situaciones reales -->
<!-- Botón con solo icono: el lector necesita saber qué hace -->
<button>
<svg aria-hidden="true">...</svg>
<span class="sr-only">Eliminar elemento</span>
</button>
<!-- Tabla con contexto que el diseño visual hace obvio pero el lector no -->
<td>
<span aria-hidden="true">★★★★☆</span>
<span class="sr-only">Valoración: 4 de 5 estrellas</span>
</td>
<!-- Enlace "leer más" que sin contexto es ambiguo -->
<a href="/blog/mi-articulo">
Leer más
<span class="sr-only">sobre "Cómo aprender a programar"</span>
</a>
<!-- Sin el sr-only, el lector anuncia múltiples "Leer más" sin distinción -->
Contraste de color
Las WCAG exigen una relación de contraste mínima entre el texto y el fondo para que personas con visión reducida o daltonismo puedan leer el contenido.
/* Requisitos de contraste (WCAG 2.1):
- Nivel AA (mínimo legal en muchos países):
Texto normal (<18pt o <14pt bold): ratio 4.5:1
Texto grande (≥18pt o ≥14pt bold): ratio 3:1
- Nivel AAA (óptimo):
Texto normal: ratio 7:1
Texto grande: ratio 4.5:1 */
/* ❌ Contraste insuficiente */
.texto-gris-claro {
color: #999999;
background: #ffffff;
/* ratio: 2.85:1 — no cumple AA */
}
/* ✅ Contraste suficiente */
.texto-gris {
color: #595959;
background: #ffffff;
/* ratio: 7.0:1 — cumple AAA */
}
/* Herramientas para verificar contraste:
- WebAIM Contrast Checker: webaim.org/resources/contrastchecker
- axe DevTools (extensión del navegador)
- Las DevTools de Chrome tienen un verificador de contraste integrado
(inspecciona un elemento de texto → en el panel Color verás el ratio) */
Modales y componentes complejos
<!-- Modal accesible: gestionar el foco correctamente -->
<div
id="modal"
role="dialog"
aria-modal="true"
aria-labelledby="modal-titulo"
tabindex="-1"
>
<h2 id="modal-titulo">Confirmar eliminación</h2>
<p>¿Estás seguro de que quieres eliminar este elemento?</p>
<button id="btn-cancelar">Cancelar</button>
<button id="btn-confirmar">Eliminar</button>
</div>
// JavaScript: gestionar el foco en modales
function abrirModal() {
const modal = document.getElementById('modal');
const botonAbrir = document.getElementById('btn-abrir');
modal.removeAttribute('hidden');
modal.focus(); // mover el foco al modal
// Atrapar el foco dentro del modal (focus trap)
const enfocables = modal.querySelectorAll(
'button, a[href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const primero = enfocables[0];
const ultimo = enfocables[enfocables.length - 1];
modal.addEventListener('keydown', function(e) {
if (e.key === 'Tab') {
if (e.shiftKey) {
// Shift+Tab: si estamos en el primero, ir al último
if (document.activeElement === primero) {
e.preventDefault();
ultimo.focus();
}
} else {
// Tab: si estamos en el último, ir al primero
if (document.activeElement === ultimo) {
e.preventDefault();
primero.focus();
}
}
}
// Cerrar con Escape
if (e.key === 'Escape') cerrarModal();
});
}
function cerrarModal() {
const modal = document.getElementById('modal');
modal.setAttribute('hidden', '');
// Devolver el foco al elemento que abrió el modal
document.getElementById('btn-abrir').focus();
}
Cómo probar la accesibilidad de tu sitio
Navegar solo con teclado. Desconecta el ratón y navega tu sitio usando solo Tab, Shift+Tab, Enter y las teclas de flecha. ¿Puedes llegar a todos los elementos interactivos? ¿Ves siempre dónde está el foco? ¿Puedes activar todos los botones y enlaces?
Usar un lector de pantalla. VoiceOver está integrado en macOS e iOS (actívalo con Cmd+F5). NVDA es gratuito para Windows. Prueba a navegar tu página sin mirar la pantalla.
Herramientas automáticas. Las herramientas automáticas detectan entre el 30% y el 40% de los problemas de accesibilidad, pero son un buen primer paso:
/* axe DevTools: extensión del navegador (Chrome y Firefox)
Abre DevTools → pestaña axe → Scan ALL of my page
Muestra problemas con explicación y sugerencias de corrección */
/* Lighthouse: integrado en Chrome DevTools
Abre DevTools → pestaña Lighthouse → Accessibility
Da una puntuación del 0 al 100 y lista los problemas */
/* WAVE: extensión del navegador
Muestra errores y avisos directamente sobre la página */
Lista de verificación rápida
- ¿Todas las imágenes tienen atributo
alt? (vacío para las decorativas) - ¿Todos los campos de formulario tienen un
<label>asociado? - ¿Los mensajes de error son específicos y están cerca del campo?
- ¿Los botones e iconos tienen texto descriptivo (visible o con
sr-only)? - ¿La jerarquía de encabezados es lógica y no salta niveles?
- ¿El foco del teclado es visible en todos los elementos interactivos?
- ¿El contraste de texto cumple al menos el nivel AA (4.5:1)?
- ¿Las acciones que requieren ratón también funcionan con teclado?
- ¿Los modales atrapan el foco y devuelven el foco al cerrarse?
- ¿Los cambios dinámicos se anuncian con
aria-liveorole="alert"?
Resumen
- La accesibilidad web garantiza que personas con discapacidades pueden usar tu sitio. Afecta a alrededor del 15% de la población y en muchos contextos es un requisito legal.
- El HTML semántico es la base: usa las etiquetas correctas (
header,nav,main,article,button,a) en lugar dedivyspanpara todo. - La jerarquía de encabezados debe ser lógica: solo un
h1, no saltar niveles, usar el nivel correcto según la estructura no según el tamaño visual. - Todas las imágenes necesitan
alt: descriptivo para imágenes informativas, vacío (alt="") para las decorativas. - Los formularios necesitan
labelpara cada campo, mensajes de error específicos y accesibles, y el atributoautocompletedonde corresponda. - El foco del teclado debe ser siempre visible. Nunca elimines el outline sin reemplazarlo. Usa
:focus-visiblepara una buena experiencia tanto con ratón como con teclado. - Usa ARIA solo cuando el HTML semántico no sea suficiente:
aria-label,aria-expanded,aria-live,role="alert"son los más habituales. - Prueba con teclado, lector de pantalla y herramientas como axe o Lighthouse. Las herramientas automáticas detectan solo el 30-40% de los problemas: la prueba manual es imprescindible.
No hay comentarios todavía. Sé el primero en compartir tu opinión.