La accesibilidad web no es un extra ni una funcionalidad de nicho. Aproximadamente el 15% de la poblacion mundial vive con alguna forma de discapacidad, y un sitio inaccesible simplemente les cierra la puerta. Mas alla del argumento etico, en muchos paises existen leyes que exigen accesibilidad digital, y los sitios accesibles tienden a tener mejor SEO porque los motores de busqueda valoran la estructura semantica. Este articulo cubre patrones concretos que puedes aplicar hoy en tu codigo.
El error mas comun en accesibilidad es usar <div> para todo. Un <div> no tiene significado semantico. Los lectores de pantalla no pueden decirle al usuario "esto es un menu de navegacion" si solo ven un <div> con links adentro.
Comparemos una estructura mal hecha con una correcta:
<!-- Mal: todo es div -->
<div class="header">
<div class="logo">Mi Sitio</div>
<div class="nav">
<div class="nav-item"><a href="/">Inicio</a></div>
<div class="nav-item"><a href="/blog">Blog</a></div>
</div>
</div>
<div class="content">
<div class="title">Titulo del articulo</div>
<div class="text">Contenido...</div>
</div>
<div class="footer">Copyright 2026</div>
<!-- Bien: HTML semantico -->
<header>
<a href="/" aria-label="Mi Sitio - Ir al inicio">Mi Sitio</a>
<nav aria-label="Navegacion principal">
<ul>
<li><a href="/">Inicio</a></li>
<li><a href="/blog">Blog</a></li>
</ul>
</nav>
</header>
<main>
<article>
<h1>Titulo del articulo</h1>
<p>Contenido...</p>
</article>
</main>
<footer>Copyright 2026</footer>
La version semantica le da al lector de pantalla toda la informacion que necesita: hay un encabezado, una navegacion principal, un contenido principal con un articulo, y un pie de pagina. El usuario puede saltar directamente a cualquiera de estas secciones.
Los encabezados (<h1> a <h6>) no son solo tamanios de texto. Son la estructura del documento. Los usuarios de lectores de pantalla los usan para navegar la pagina, saltando de encabezado en encabezado como si fuera un indice.
Reglas fundamentales:
<h1> por pagina (el titulo principal).<h1> viene <h2>, no <h3>.ARIA (Accessible Rich Internet Applications) es un conjunto de atributos que agrega informacion de accesibilidad al HTML. La primera regla de ARIA es: si puedes usar HTML nativo, no uses ARIA.
Un <button> nativo ya es accesible: tiene rol de boton, es navegable por teclado y anunciado correctamente por lectores de pantalla. Agregarle role="button" es redundante. Pero si por alguna razon usas un <div> como boton (algo que deberias evitar), necesitas ARIA:
<!-- Si DEBES usar un div como boton (evitalo si puedes) -->
<div
role="button"
tabindex="0"
aria-label="Cerrar modal"
onkeydown="if(event.key==='Enter'||event.key===' ') handleClick()"
onclick="handleClick()"
>
X
</div>
<!-- Mucho mejor: un boton nativo -->
<button aria-label="Cerrar modal" onclick="handleClick()">
X
</button>
Observa todo el trabajo extra que requiere el <div>: necesita role, tabindex, aria-label y manejo de teclado manual. El <button> hace todo esto gratis.
Atributos ARIA que si usaras frecuentemente:
aria-label: etiqueta invisible que describe el elemento. Ideal para botones con solo un icono.aria-labelledby: apunta al ID de otro elemento que sirve como etiqueta.aria-describedby: apunta a un elemento con informacion adicional (como instrucciones de un campo de formulario).aria-hidden="true": oculta un elemento del arbol de accesibilidad. Util para iconos decorativos.aria-live: anuncia cambios dinamicos al contenido. Valores: polite (espera a que el usuario termine) o assertive (interrumpe).aria-expanded: indica si un elemento colapsable esta abierto o cerrado.Los formularios son una de las areas con mas problemas de accesibilidad. Cada campo necesita una etiqueta asociada explicitamente:
<!-- Mal: placeholder no es un label -->
<input type="email" placeholder="Tu email">
<!-- Bien: label asociado con for/id -->
<label for="email">Correo electronico</label>
<input type="email" id="email" name="email" required
aria-describedby="email-help email-error">
<span id="email-help">Usaremos este correo para enviarte actualizaciones.</span>
<span id="email-error" role="alert" aria-live="polite"></span>
Puntos clave para formularios:
<label> vinculado al campo con for/id. El placeholder desaparece al escribir y no es anunciado por todos los lectores de pantalla.required nativo y complementa con aria-required="true" si necesitas soporte mas amplio.role="alert" y aria-live="polite" para que los lectores de pantalla anuncien errores dinamicamente. Vincula el mensaje al campo con aria-describedby.<fieldset> y <legend>:<fieldset>
<legend>Metodo de pago</legend>
<label>
<input type="radio" name="pago" value="tarjeta"> Tarjeta de credito
</label>
<label>
<input type="radio" name="pago" value="transferencia"> Transferencia bancaria
</label>
</fieldset>
Muchos usuarios no pueden usar un mouse. Toda funcionalidad de tu sitio debe ser alcanzable y operable con teclado.
Nunca hagas esto:
/* NUNCA */
*:focus { outline: none; }
El outline de focus es la unica forma que tienen los usuarios de teclado de saber donde estan en la pagina. Si el estilo por defecto no te gusta, reemplazalo, pero no lo elimines:
/* Mejor: estilo personalizado pero visible */
:focus-visible {
outline: 3px solid #2563eb;
outline-offset: 2px;
}
Nota el uso de :focus-visible en lugar de :focus. Esto aplica el estilo solo cuando el foco viene del teclado, no del mouse, lo que da una experiencia visual mas limpia para usuarios de mouse sin perjudicar la accesibilidad.
Los skip links permiten a usuarios de teclado saltar directamente al contenido principal sin tener que tabular por todo el menu de navegacion en cada pagina:
<body>
<a href="#main-content" class="skip-link">Saltar al contenido principal</a>
<header><!-- navegacion extensa --></header>
<main id="main-content" tabindex="-1">
<!-- contenido -->
</main>
</body>
.skip-link {
position: absolute;
top: -100%;
left: 0;
padding: 0.5rem 1rem;
background: #1a1a2e;
color: white;
z-index: 100;
}
.skip-link:focus {
top: 0;
}
El link es invisible hasta que el usuario presiona Tab al cargar la pagina.
Cuando abres un modal, el foco debe quedar atrapado dentro de el. Si el usuario presiona Tab en el ultimo elemento del modal, debe volver al primero, no salir al contenido de atras.
function trapFocus(modal) {
const focusable = modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0];
const last = focusable[focusable.length - 1];
modal.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeModal();
return;
}
if (e.key !== 'Tab') return;
if (e.shiftKey) {
if (document.activeElement === first) {
e.preventDefault();
last.focus();
}
} else {
if (document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
});
first.focus();
}
Ademas, al abrir el modal debes agregar aria-hidden="true" al contenido de fondo, y al cerrarlo, devolver el foco al elemento que lo abrio.
Las WCAG definen ratios minimos de contraste entre texto y fondo:
Un texto gris claro (#999) sobre fondo blanco tiene un ratio de solo 2.85:1, insuficiente incluso para AA. Herramientas como el contrast checker de WebAIM te permiten verificar combinaciones de colores rapidamente.
No dependas solo del color para comunicar informacion. Un mensaje de error no debe ser rojo unicamente; agrega un icono, un prefijo de texto o un borde para que usuarios con daltonismo puedan identificarlo.
Toda imagen necesita un atributo alt. Pero no todas las imagenes necesitan el mismo tipo de alt:
alt="Grafico de barras mostrando crecimiento de ventas del 40% en Q3 2025".alt="" (vacio, no omitido). Esto le dice al lector de pantalla que la ignore.alt debe describir el destino, no la imagen. alt="Perfil de GitHub", no alt="Logo de GitHub".<!-- Informativa -->
<img src="arquitectura.png" alt="Diagrama de arquitectura mostrando el flujo
desde el cliente React, pasando por la API en Node.js, hasta la base de
datos PostgreSQL">
<!-- Decorativa -->
<img src="divider.svg" alt="" aria-hidden="true">
<!-- Link -->
<a href="https://github.com/user">
<img src="github-icon.svg" alt="Perfil de GitHub">
</a>
<button
aria-expanded="false"
aria-controls="main-menu"
aria-label="Abrir menu de navegacion"
id="menu-toggle"
>
<span aria-hidden="true">☰</span>
</button>
<nav id="main-menu" hidden>
<ul role="list">
<li><a href="/">Inicio</a></li>
<li><a href="/blog">Blog</a></li>
<li><a href="/contacto">Contacto</a></li>
</ul>
</nav>
Al abrir el menu, actualiza aria-expanded="true", remueve el atributo hidden y cambia el aria-label a "Cerrar menu de navegacion".
<div class="accordion">
<h3>
<button aria-expanded="false" aria-controls="panel-1">
Pregunta frecuente 1
</button>
</h3>
<div id="panel-1" role="region" hidden>
<p>Respuesta detallada aqui.</p>
</div>
</div>
El patron es consistente: un <button> con aria-expanded que controla la visibilidad de un panel vinculado con aria-controls.
No necesitas ser experto en accesibilidad para detectar problemas. Estas herramientas automatizan gran parte del analisis:
Una prueba rapida que cualquier desarrollador puede hacer: desconecta tu mouse y navega tu sitio completo usando solo Tab, Shift+Tab, Enter y Escape. Si no puedes completar todas las acciones, hay un problema de accesibilidad.
La accesibilidad no se logra con un sprint dedicado al final del proyecto. Se construye desde el primer commit, eligiendo los elementos HTML correctos y siendo consciente de que no todos los usuarios interactuan con tu sitio de la misma forma. Los patrones mostrados aqui cubren la gran mayoria de problemas comunes, y aplicarlos de forma consistente hara una diferencia real para miles de usuarios.