Accesibilidad (a11y)
La accesibilidad web significa que personas con discapacidades puedan percibir, entender, navegar e interactuar con la web. No es un "nice-to-have" ni un feature opcional: es un requisito fundamental que beneficia a todos. Las buenas prácticas de accesibilidad mejoran el SEO, la usabilidad en móvil, la experiencia para usuarios con conexión lenta y la calidad general de tu código. Escribir HTML accesible simplemente significa escribir HTML correcto.
Principios WCAG
Las WCAG (Web Content Accessibility Guidelines) son el estándar internacional de accesibilidad web, publicadas por el W3C. Están organizadas en torno a cuatro principios fundamentales, conocidos como POUR. Cada principio agrupa pautas que aseguran que el contenido web sea accesible para la mayor cantidad de personas posible, independientemente de sus capacidades físicas, cognitivas o tecnológicas. Las WCAG tienen tres niveles de conformidad: A (mínimo), AA (recomendado, el estándar que la mayoría de las leyes exigen) y AAA (el más alto).
Perceptible
La información y los componentes de la interfaz deben ser presentables de forma que los usuarios puedan percibirlos. Texto alternativo en imágenes, subtítulos en video, suficiente contraste de colores, contenido adaptable a diferentes presentaciones (audio, braille, texto grande). Si un usuario no puede ver, tiene que poder escuchar o leer el contenido por otro medio.
Operable
Los componentes de la interfaz y la navegación deben ser operables. Esto significa que todo debe poder usarse con teclado, que haya suficiente tiempo para leer y usar el contenido, que no haya contenido que cause seizures (flashes), y que los usuarios puedan navegar y encontrar contenido. Si un usuario no puede usar un mouse, tiene que poder hacer todo con el teclado.
Comprensible
La información y la operación de la interfaz deben ser comprensibles. El lenguaje debe ser claro y predecible, la funcionalidad debe ser consistente (no cambies el comportamiento de un botón según el contexto), y los errores deben ser detectados y explicados de forma clara. Los formularios deben tener labels, las instrucciones deben ser explícitas y el idioma del contenido debe ser identificable.
Robusto
El contenido debe ser lo suficientemente robusto como para ser interpretado por una amplia variedad de agentes de usuario, incluyendo tecnologías assistivas. HTML semántico, atributos ARIA correctos, compatibilidad con lectores de pantalla (NVDA, JAWS, VoiceOver), y manejo de errores graceful (el sitio no se rompe si un script falla). El contenido debe funcionar tanto en Chrome como en un lector de pantalla.
| Nivel | Descripción | Ejemplos |
|---|---|---|
| A | Mínimo indispensable | Texto alternativo en imágenes, labels en formularios, navegación por teclado |
| AA | Estándar recomendado (legalmente requerido en muchos países) | Contraste 4.5:1, resize de texto hasta 200%, sin traps de teclado |
| AAA | Nivel máximo de accesibilidad | Contraste 7:1, sign language en video, ubicación del focus visible |
Atributos ARIA
ARIA (Accessible Rich Internet Applications) es un conjunto de atributos HTML que complementan la semántica nativa cuando HTML no es suficiente. Se usa cuando creas componentes custom (un modal, un tabs, un accordion, un dropdown) que no tienen una etiqueta HTML nativa equivalente. ARIA no agrega funcionalidad: solo le dice a las tecnologías assistivas qué es cada elemento, qué estado tiene y cómo se relaciona con otros. Es el puente entre tu JavaScript interactivo y el lector de pantalla.
La regla de oro de ARIA es: "No uses ARIA si podés usar HTML semántico". Un <button> nativo ya tiene el role "button", maneja focus, responde a Enter y Space, y los lectores de pantalla lo anuncian correctamente. Si creas un <div onclick="..."> y le agregas role="button", todavía necesitas agregar tabindex="0" y manejar Enter/Space con JavaScript. Usar el <button> nativo es infinitamente más simple y correcto.
Roles, estados y propiedades
Los atributos ARIA se dividen en tres categorías. Los roles (role="...") definen qué es un elemento (banner, navigation, dialog, alert, tablist, tree). Los estados (aria-checked, aria-expanded, aria-disabled, aria-hidden) describen la condición actual del elemento y cambian dinámicamente con JavaScript. Las propiedades (aria-label, aria-labelledby, aria-describedby, aria-live) relacionan el elemento con otros nodos del DOM o proporcionan texto descriptivo.
<!-- aria-label: texto descriptivo para elementos sin texto visible -->
<button aria-label="Cerrar diálogo">
<svg>...</svg>
</button>
<!-- aria-labelledby: referencia a otro elemento como label -->
<section aria-labelledby="titulo-seccion">
<h2 id="titulo-seccion">Novedades</h2>
<p>Contenido de novedades...</p>
</section>
<!-- aria-describedby: texto de ayuda adicional -->
<input type="email" aria-describedby="email-help">
<small id="email-help">Tu email no será compartido</small>
<!-- aria-expanded: para acordeones y dropdowns -->
<button aria-expanded="false" aria-controls="menu-id">
Menú
</button>
<div id="menu-id" role="menu" hidden>...</div>
<!-- aria-hidden: ocultar contenido decorativo de lectores -->
<span class="icono" aria-hidden="true">+</span>
<span class="sr-only">Agregar item</span>
<!-- aria-live: anunciar cambios dinámicos -->
<div aria-live="polite" aria-atomic="true">
3 resultados encontrados.
</div>
<!-- Roles para componentes custom -->
<div role="dialog" aria-modal="true" aria-labelledby="dialog-title">
<h2 id="dialog-title">Confirmar acción</h2>
<p>¿Estás seguro?</p>
</div>
<!-- Tabs accesibles -->
<div role="tablist">
<button role="tab" aria-selected="true"
aria-controls="panel-1" id="tab-1">
Tab 1
</button>
<button role="tab" aria-selected="false"
aria-controls="panel-2" id="tab-2">
Tab 2
</button>
</div>
<div role="tabpanel" id="panel-1"
aria-labelledby="tab-1">Contenido 1</div>
<div role="tabpanel" id="panel-2"
aria-labelledby="tab-2" hidden>Contenido 2</div>
| Atributo ARIA | Categoría | Propósito |
|---|---|---|
role |
Rol | Define qué es el elemento (button, dialog, navigation, tablist) |
aria-label |
Propiedad | Texto descriptivo cuando no hay texto visible en el elemento |
aria-labelledby |
Propiedad | Apunta al ID de otro elemento que funciona como label |
aria-describedby |
Propiedad | Apunta al ID de un elemento con descripción adicional |
aria-expanded |
Estado | Indica si un elemento está abierto o cerrado (true/false) |
aria-hidden |
Estado | Oculta el elemento de las tecnologías assistivas |
aria-live |
Propiedad | Anuncia cambios dinámicos (off, polite, assertive) |
aria-selected |
Estado | Indica si un item está seleccionado (tabs, listbox) |
aria-controls |
Propiedad | Apunta al ID del elemento que controla este botón |
aria-modal |
Estado | Indica si un dialog es modal (bloquea el resto de la página) |
La regla de los 5 ARIA: First Rule of ARIA
Si podés usar un elemento HTML nativo con la semántica y el comportamiento incorporados, usálo en lugar de ARIA. Un <button> nativo ya es accesible. Un <nav> ya tiene role "navigation". Un <input type="checkbox"> ya tiene role "checkbox" y estado toggleable. Solo usa ARIA cuando no existe una etiqueta HTML nativa para lo que necesitás (menus custom, dialogs, tooltips, sliders personalizados).
Contraste y tipografía
El contraste de colores es probablemente el aspecto de accesibilidad más visible y fácil de verificar. Las WCAG especifican ratios mínimos de contraste entre el texto y su fondo para asegurar legibilidad. El ratio se mide con la fórmula de luminancia relativa (WCAG 2.x) y va de 1:1 (sin contraste, mismo color) a 21:1 (contraste máximo, negro sobre blanco). Las personas con baja visión, daltonismo o que leen en pantallas con brillo (al aire libre, bajo la luz del sol) dependen directamente de un buen contraste.
Para nivel AA: el texto normal necesita un ratio mínimo de 4.5:1 y el texto grande (18px+ o 14px+ en bold) necesita 3:1. Para nivel AAA: el texto normal necesita 7:1 y el texto grande 4.5:1. Para elementos de interfaz (bordes, iconos, focus indicators) el mínimo es 3:1. Un gris claro sobre blanco (#999 sobre #fff) tiene un ratio de solo 2.85:1 y no cumple AA para texto. El gris de WebForge (#8b949e sobre #0d1117) tiene un ratio de 4.68:1, que cumple AA.
| Elemento | Nivel AA | Nivel AAA |
|---|---|---|
| Texto normal (<18px) | 4.5:1 | 7:1 |
| Texto grande (18px+ o 14px bold) | 3:1 | 4.5:1 |
| Elementos de UI (bordes, iconos) | 3:1 | No aplica |
/* Buena practica: tipografia escalable */
html {
/* Permite que el usuario ajuste el tamaño */
font-size: 100%; /* 16px por defecto */
}
/* Usa rem, nunca px fijo para texto */
p, span, a {
font-size: 1rem; /* 16px relativo al root */
line-height: 1.6; /* Espaciado generoso */
}
/* Texto grande con buen contraste */
h1 {
font-size: 2.5rem;
color: var(--text-primary); /* Asegurar contraste 3:1+ */
}
/* Nunca uses text-decoration con color sin contraste */
a {
color: var(--accent-blue);
text-decoration: underline; /* Ayuda a identificar links */
}
/* Focus visible claro para navegacion por teclado */
*:focus-visible {
outline: 3px solid var(--accent-cyan);
outline-offset: 2px;
}
/* Remover outline solo con :focus (no :focus-visible) es mala practica */
/* MAL: *:focus { outline: none; } */
/* BIEN: *:focus:not(:focus-visible) { outline: none; } */
Herramientas para verificar contraste
Usa WebAIM Contrast Checker (webaim.org/resources/contrastchecker) para verificar tus combinaciones de color. Chrome DevTools tiene un checker integrado: selecciona un elemento, ve a Color Picker y muestra el ratio. Stark es un plugin de Figma que verifica contraste en tiempo real. Axe DevTools (extensión de Chrome) audita contraste automáticamente junto con otros problemas de accesibilidad.
Navegación por teclado
Muchas personas no pueden usar un mouse: usuarios con discapacidades motoras, usuarios de screen readers que navegan secuencialmente, personas con RSI (lesiones por esfuerzo repetitivo), desarrolladores power users, y simplemente cualquiera que prefiera el teclado. La navegación por teclado no es un feature opcional: es la base de la accesibilidad operable. Si algo no funciona con teclado, no es accesible, punto.
Los elementos interactivos nativos (<a>, <button>, <input>, <select>, <textarea>) son focusable por defecto y responden a teclas correctas. El problema surge cuando creas componentes custom con <div> o <span>: un <div onclick="..."> no es focusable, no responde a Enter ni Space, y los lectores de pantalla no lo anuncian como interactivo. Para estos casos, necesitas tabindex y JavaScript para simular el comportamiento nativo.
| Tecla | Acción |
|---|---|
| Tab | Moverse al siguiente elemento focusable |
| Shift + Tab | Moverse al elemento focusable anterior |
| Enter | Activar links y botones |
| Space | Activar botones, toggle checkboxes |
| Escape | Cerrar modals, dropdowns, menus |
| Flechas | Navegar dentro de tabs, menus, radios |
| Home / End | Ir al primero/último item de una lista |
<!-- MAL: div como boton (no focusable, no responde a teclado) -->
<div class="btn" onclick="abrirModal()">Abrir</div>
<!-- BIEN: button nativo (focusable, Enter/Space, lector de pantalla) -->
<button onclick="abrirModal()">Abrir</button>
<!-- SI NECESITAS un div como boton (evitar si es posible) -->
<div tabindex="0" role="button"
onclick="abrirModal()"
onkeydown="if(event.key==='Enter'||event.key===' ')
{event.preventDefault();abrirModal()}">
Abrir
</div>
<!-- tabindex valores -->
<!-- tabindex="-1": focusable via JS (programmatic), no via Tab -->
<!-- tabindex="0": focusable, se agrega al orden natural del tab -->
<!-- tabindex="5": focusable, orden personalizado (EVITAR) -->
<!-- Skip link: permite saltar al contenido principal -->
<a href="#main-content" class="skip-link">
Saltar al contenido principal
</a>
/* Skip link: visible solo con teclado */
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: var(--accent-blue);
color: white;
padding: 8px 16px;
z-index: 100;
font-weight: 600;
}
.skip-link:focus {
top: 0; /* Aparece cuando recibe foco */
}
/* Focus visible: mostrar outline solo con teclado */
/* Esto oculta el outline en clicks de mouse
pero lo mantiene con teclado */
*:focus:not(:focus-visible) {
outline: none;
}
*:focus-visible {
outline: 3px solid var(--accent-cyan);
outline-offset: 2px;
}
/* Evitar tabindex > 0: rompe el orden natural */
/* MAL: tabindex="5" */
/* El orden del tab debe ser lógico y natural,
siguiendo el orden visual del contenido */
Nunca uses tabindex mayor a 0
Usar tabindex="3" o cualquier valor positivo fuerza un orden de tabulación que no necesariamente coincide con el orden visual. Si reordenas elementos visualmente con CSS, el tabindex se desincroniza. Además, los valores positivos interrumpen el flujo natural del tab para TODOS los elementos que le siguen. Si necesitas cambiar el orden, reordena el HTML. Solo usa tabindex="0" (agregar al flujo natural) y tabindex="-1" (foco programático con JavaScript, no incluido en el tab).
Screen readers y contenido oculto
Los screen readers son software que lee el contenido de la pantalla en voz alta o lo convierte a braille. Los más usados son NVDA (Windows, gratis, open source), JAWS (Windows, pago, el más usado en empresas), VoiceOver (macOS e iOS, integrado en el sistema) y TalkBack (Android, integrado). Cada uno tiene sus particularidades, pero todos se basan en el mismo DOM y los mismos atributos ARIA. Si tu HTML es semántico, todos los screen readers lo manejan bien sin ARIA adicional.
Un patrón fundamental es el contenido visible solo para lectores de pantalla, usando la clase .sr-only (screen reader only) o .visually-hidden. Esto es texto que existe en el DOM y los lectores lo anuncian, pero está oculto visualmente con CSS. Se usa para: dar contexto a iconos solos ("×" para cerrar necesita un "Cerrar" textual), anunciar estados ("2 de 5", "Cargando..."), labels cortos ("Buscar" en un botón que solo tiene un icono de lupa), y textos auxiliares que no tienen cabida visual pero que son necesarios para entender el contexto.
/* Clase sr-only: visible para lectores, oculto visualmente */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
/* Variante: sr-only que se vuelve visible con foco
(para skip links, por ejemplo) */
.sr-only-focusable:focus {
position: static;
width: auto;
height: auto;
padding: inherit;
margin: inherit;
overflow: visible;
clip: auto;
white-space: normal;
}
<!-- Icono con texto para lectores de pantalla -->
<button type="button">
<svg aria-hidden="true">...</svg>
<span class="sr-only">Buscar</span>
</button>
<!-- Icono + texto visible + texto auxiliar oculto -->
<div>
<span>Precio:</span>
<strong>$1.500</strong>
<span class="sr-only">por mes</span>
</div>
<!-- Estado de una notificacion -->
<div class="notification">
<span class="badge">3</span>
<span class="sr-only">mensajes nuevos</span>
</div>
<!-- aria-hidden vs sr-only -->
<!-- aria-hidden: OCULTA del lector (iconos decorativos) -->
<!-- sr-only: MUESTRA al lector, oculta visualmente -->
<!-- No uses aria-hidden="true" en texto real -->
<!-- No uses display:none para sr-only (lo saca del DOM) -->
display: none vs visibility: hidden vs sr-only
display: none y visibility: hidden sacan el elemento completamente del flujo y los lectores de pantalla no lo leen. Son para contenido que nadie debe ver ni oír. .sr-only mantiene el elemento en el DOM (los lectores lo leen) pero lo oculta visualmente. aria-hidden="true" oculta solo para lectores de pantalla pero sigue visible visualmente. Son herramientas diferentes para propósitos diferentes: no las mezcles.
Imágenes accesibles
Las imágenes son uno de los aspectos más críticos de la accesibilidad. Cada <img> debe tener un atributo alt que describa su contenido. La pregunta clave es: "si esta imagen no cargara, ¿qué texto necesitaría el usuario para no perder información?". El alt no es "SEO text" ni una descripción del archivo: es la información que la imagen transmite. Una foto de un equipo de trabajo en la sección "Sobre nosotros" necesita un alt descriptivo. Un ícono decorativo de una flecha necesita alt="" (alt vacío) para que el lector lo salte.
<!-- BIEN: imagen informativa con alt descriptivo -->
<img src="equipo.jpg"
alt="Equipo de desarrollo trabajando en la oficina,
tres personas frente a monitores con código">
<!-- BIEN: imagen decorativa con alt vacio -->
<img src="divider.png" alt="" role="presentation">
<!-- BIEN: logo que linkea al inicio -->
<a href="/">
<img src="logo.svg" alt="WebForge - Inicio">
</a>
<!-- BIEN: imagen con texto largo: usa figure + figcaption -->
<figure>
<img src="arquitectura.png"
alt="Diagrama de la arquitectura MVC:
Modelo, Vista y Controlador separados">
<figcaption>
Figura 1: Arquitectura MVC clásica.
El Controller recibe requests, actualiza el Model
y la View renderiza la respuesta.
</figcaption>
</figure>
<!-- MAL: alt que dice "imagen" o "foto" (no aporta info) -->
<img src="gato.jpg" alt="imagen de un gato">
<!-- MAL: alt con keywords para SEO -->
<img src="gato.jpg"
alt="gato bonito gatito mascot felino adorable">
<!-- MAL: omitir alt completamente -->
<img src="gato.jpg">
| Tipo de imagen | Alt | Ejemplo |
|---|---|---|
| Informativa (foto, diagrama) | Descripción del contenido | alt="Mapa de red con 3 servidores conectados" |
| Decorativa (dividers, fondos) | alt="" (vacío) |
alt="" |
| Funcional (botón con imagen) | Acción que realiza | alt="Enviar formulario" |
| Con texto dentro (captura, meme) | Reproducir el texto de la imagen | alt="Error 404: Página no encontrada" |
| Compleja (gráfico, infografía) | Resumen + figcaption detallada | alt="Gráfico de ventas 2024" + figcaption |
Herramientas de testing
No podés mejorar lo que no podés medir. Las herramientas de testing de accesibilidad te permiten detectar problemas automáticamente, auditar tu código de forma continua e integrar la accesibilidad en tu flujo de trabajo. Es importante entender que ninguna herramienta automatizada detecta el 100% de los problemas de accesibilidad: una herramienta puede verificar que tienes alt en las imágenes, pero no puede decirte si el texto del alt es descriptivo o si es "image.jpg". El testing manual con un lector de pantalla sigue siendo insustituible.
Lighthouse
Integrado en Chrome DevTools (tab Lighthouse). Audita accesibilidad junto con performance, SEO y best practices. Genera un score de 0-100 con problemas específicos y recomendaciones. Ideal para audits rápidos y checkpoints en tu desarrollo. Se ejecuta con Ctrl+Shift+I → Lighthouse → Generar reporte.
Axe DevTools
Extensión de Chrome/Edge de Deque. Escanea automáticamente la página y lista problemas de accesibilidad por severidad (critical, serious, moderate, minor). También disponible como plugin de VS Code, integración con CI/CD, y API para testing automatizado. Es la herramienta más completa para testing manual guiado.
WAVE
Herramienta online (wave.webaim.org) o extensión de navegador. Analiza cualquier página y visualiza los errores directamente sobre la página con iconos rojos (errores), amarillos (advertencias) y verdes (features accesibles). Es visualmente intuitivo y perfecto para entender dónde están los problemas en tu layout real.
VoiceOver / NVDA
Testing manual con lectores de pantalla reales. En macOS: Cmd+F5 para activar VoiceOver. En Windows: descargar NVDA gratis. Navega tu sitio usando solo teclado y escucha cómo el lector anuncia cada elemento. Es la única forma de detectar problemas de flujo, contexto y experiencia real que las herramientas automatizadas no pueden encontrar.
Checklist rápido de accesibilidad
Antes de entregar cualquier página, verifica: (1) Todas las imágenes tienen alt descriptivo o alt="" si son decorativas. (2) Todo se puede operar con solo teclado (Tab, Enter, Escape). (3) Los colores tienen contraste mínimo 4.5:1 para texto normal. (4) Los formularios tienen <label> con for. (5) Hay un skip link al contenido principal. (6) Los modals atrapan el foco y se cierran con Escape. (7) lang está seteado en el <html>. (8) El heading hierarchy es lógico (H1 → H2 → H3, sin saltar niveles).