Fundamentos de accesibilidad web para desarrolladores
Patrones prácticos de accesibilidad que mejoran la usabilidad para todas las personas y mantienen tu aplicación conforme y semánticamente correcta.

La mayoría de las correcciones de accesibilidad no son refactorizaciones heroicas. Son cambios pequeños y vergonzosamente simples que dejaste pasar porque nadie se quejó... todavía. La realidad es que entre el 15 y el 20 % de los usuarios tiene algún tipo de discapacidad, y las interfaces inaccesibles los alejan silenciosamente sin que lleguen siquiera a reportar un error.
La accesibilidad no es una función que se agrega después. Es un estándar de calidad básico, igual que el manejo de errores o la escritura de pruebas.
El HTML semántico es la base
La mejora de accesibilidad de mayor impacto no cuesta ningún esfuerzo extra: usar los elementos HTML correctos. Un <button> ya se encarga del foco por teclado, la activación con Enter/Espacio y los anuncios del lector de pantalla. Un <div onClick> no hace nada de eso.
<!-- ❌ Inaccessible — no keyboard support, no role, no focus -->
<div class="btn" onclick="handleClick()">
Submit
</div>
<!-- ✅ Accessible by default — focus, keyboard, screen reader support -->
<button type="submit" onclick="handleClick()">
Submit
</button>El mismo principio aplica en todas partes: <nav> en lugar de <div class="nav">, <main> en lugar de <div class="content">, <h2> en lugar de <div class="heading">. Cada elemento semántico lleva roles ARIA implícitos en los que se apoya la tecnología de asistencia.
Errores semánticos comunes
<!-- ❌ Heading hierarchy skipped — confuses screen reader navigation -->
<h1>Page Title</h1>
<h4>Section Title</h4>
<!-- ✅ Sequential heading levels -->
<h1>Page Title</h1>
<h2>Section Title</h2>Los lectores de pantalla generan una tabla de contenidos a partir de los encabezados. Saltarse niveles crea un esquema roto que hace la navegación dolorosa para quienes dependen de ella.
Patrones de navegación por teclado
Todo elemento interactivo debe poder alcanzarse y operarse solo con el teclado. Esto implica gestionar el orden de foco, los indicadores de foco visibles y los manejadores de eventos de teclado.
/* ❌ Destroys keyboard accessibility — users can't see where they are */
*:focus {
outline: none;
}
/* ✅ Custom focus style that's visible and on-brand */
*:focus-visible {
outline: 2px solid #4A90D9;
outline-offset: 2px;
border-radius: 2px;
}La pseudoclase :focus-visible es la solución correcta: solo muestra el contorno para la navegación por teclado, no para los clics del mouse. Esto responde a la queja de diseño sobre "contornos feos" sin sacrificar la accesibilidad.
Atrapar el foco en modales
Cuando se abre un modal, el foco debe permanecer dentro de él. De lo contrario, los usuarios de teclado tabulan detrás del modal hacia contenido invisible.
function trapFocus(modalElement: HTMLElement) {
const focusableSelectors = [
'button', '[href]', 'input', 'select',
'textarea', '[tabindex]:not([tabindex="-1"])'
];
const focusable = modalElement.querySelectorAll(
focusableSelectors.join(', ')
);
const first = focusable[0] as HTMLElement;
const last = focusable[focusable.length - 1] as HTMLElement;
modalElement.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.key !== 'Tab') return;
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
});
first.focus();
}El HTML moderno ofrece <dialog> con atrapamiento de foco incorporado, pero los modales personalizados todavía necesitan una gestión explícita.
ARIA: úsalo con moderación y precisión
Los atributos ARIA deben llenar los vacíos donde la semántica de HTML se queda corta, no reemplazarla. La primera regla de ARIA es literalmente "no uses ARIA" si un elemento HTML nativo ya hace el trabajo.
<!-- ❌ Redundant — button already has role="button" -->
<button role="button" aria-label="Submit">Submit</button>
<!-- ✅ ARIA adds missing context where needed -->
<button aria-expanded="false" aria-controls="dropdown-menu">
Options
</button>
<ul id="dropdown-menu" role="menu" hidden>
<li role="menuitem">Edit</li>
<li role="menuitem">Delete</li>
</ul>Los atributos ARIA más útiles para el desarrollo cotidiano:
aria-label— etiqueta elementos que no tienen texto visiblearia-expanded— comunica el estado de un elemento desplegablearia-live— anuncia cambios de contenido dinámicoaria-hidden="true"— oculta elementos decorativos de los lectores de pantalla
Regiones activas para contenido dinámico
Cuando el contenido se actualiza sin recargar la página (notificaciones tipo toast, validación de formularios, datos en vivo), los lectores de pantalla necesitan una notificación explícita.
function announceMessage(message: string, priority: 'polite' | 'assertive' = 'polite') {
const announcer = document.getElementById('live-announcer');
if (!announcer) return;
announcer.setAttribute('aria-live', priority);
announcer.textContent = '';
// Force DOM to register the empty state before updating
requestAnimationFrame(() => {
announcer.textContent = message;
});
}Coloca una región activa invisible en tu layout una sola vez y reutilízala para todos los anuncios dinámicos.
Contraste de color y diseño visual
WCAG 2.1 exige una relación de contraste mínima de 4.5:1 para texto normal y 3:1 para texto grande. Esto no es una sugerencia: es la diferencia entre legible e ilegible para millones de usuarios.
/* ❌ Fails contrast — ratio 2.3:1 */
.subtle-text {
color: #B0B0B0;
background: #FFFFFF;
}
/* ✅ Passes AA contrast — ratio 4.6:1 */
.subtle-text {
color: #767676;
background: #FFFFFF;
}Nunca dependas solo del color para comunicar significado. Los estados de error necesitan tanto el color rojo como un ícono o etiqueta de texto. Los indicadores de estado necesitan diferencias de forma o patrón, no solo de color.
Formularios: el campo minado de la accesibilidad
Los formularios acumulan más violaciones de accesibilidad que cualquier otro componente. La solución casi siempre consiste en conectar las etiquetas con los campos.
<!-- ❌ No programmatic label — screen reader says "edit text" -->
<input type="email" placeholder="Enter your email" />
<!-- ✅ Explicit label association -->
<label for="email-input">Email address</label>
<input id="email-input" type="email" placeholder="you@example.com" />Los placeholders no son etiquetas. Desaparecen al enfocar el campo, no cumplen los requisitos de contraste y no aportan contexto persistente. Cada campo necesita un <label> con un atributo for que coincida.
Para los mensajes de error, usa aria-describedby para conectar el error con el campo:
<label for="password">Password</label>
<input
id="password"
type="password"
aria-describedby="password-error"
aria-invalid="true"
/>
<span id="password-error" role="alert">
Password must be at least 8 characters
</span>Pruebas de accesibilidad
Las herramientas automatizadas detectan alrededor del 30 % de los problemas de accesibilidad. El resto requiere pruebas manuales. Una estrategia de pruebas razonable combina ambas.
Automatizado: ejecuta auditorías de accesibilidad con axe-core o Lighthouse en tu CI. Detectan lo más evidente: texto alternativo faltante, asociaciones de etiquetas rotas, fallos de contraste.
Verificaciones manuales:
- Recorre toda la página con Tab — ¿puedes alcanzar y operar todo?
- Usa un lector de pantalla (VoiceOver, NVDA) al menos para los flujos críticos
- Haz zoom al 200 % — ¿el layout se sigue viendo bien?
- Desactiva el CSS — ¿el orden del contenido sigue siendo lógico?
El árbol de accesibilidad de las DevTools del navegador muestra exactamente lo que ve la tecnología de asistencia. Si un elemento falta en el árbol o tiene el rol incorrecto, ahí está el error.
Ideas clave
- El HTML semántico resuelve el 50 % de los problemas de accesibilidad — usa los elementos correctos antes de recurrir a ARIA
- Nunca elimines los contornos de foco — usa
:focus-visiblepara un estilo visible solo con teclado - Las etiquetas son obligatorias — todo campo de formulario necesita un
<label>explícito, no solo un placeholder - El color por sí solo no basta — combina siempre el color con texto, íconos o patrones
- Prueba con teclado y lectores de pantalla — las herramientas automatizadas solo detectan una fracción de los problemas reales
- ARIA es el último recurso — la primera regla de ARIA es no usar ARIA cuando el HTML nativo funciona


