Skip to content

Accesibilidad ​

La accesibilidad web (a menudo abreviada a11y) consiste en que todas las personas puedan usar tu sitio: quienes no ven, no oyen, no pueden usar un mouse, tienen baja visión, daltonismo o dificultades cognitivas. En esta lección aprenderás las técnicas que separan un sitio amateur de uno profesional, y cómo probarlas.

Concepto ​

Piensa en una rampa en la entrada de un edificio. Fue pensada para sillas de ruedas, pero también la usan personas con carriolas, maletas o un pie lastimado. En la web pasa igual: los subtítulos ayudan a quien ve videos en silencio; el buen contraste, a quien usa el celular bajo el sol; la navegación por teclado, a quien tiene el mouse descompuesto.

Algunos datos para dimensionarlo:

  • Alrededor del 16% de la población mundial vive con alguna discapacidad (OMS).
  • En varios países la accesibilidad es obligación legal para ciertos sitios (por ejemplo, en la Unión Europea con la Ley Europea de Accesibilidad, y en EE. UU. hay demandas frecuentes por la ADA).
  • El estándar internacional son las pautas WCAG (nivel AA es el objetivo habitual).

La buena noticia: la mayor parte de la accesibilidad sale gratis si escribes HTML semántico correcto.

Cómo navegan las personas ​

  • Lector de pantalla (NVDA y JAWS en Windows, VoiceOver en Mac/iPhone, TalkBack en Android): lee el contenido en voz alta y permite saltar entre encabezados, enlaces, regiones y campos.
  • Teclado: Tab avanza, Shift+Tab retrocede, Enter activa enlaces y botones, Espacio marca casillas y presiona botones, las flechas se mueven entre radios y opciones.
  • Zoom y ampliadores: aumentan el texto al 200% o más.
  • Control por voz: "clic en Enviar" (requiere que el texto visible coincida con el nombre accesible).

Ejemplo interactivo ​

Haz clic dentro de la demo y navega solo con el teclado:

Skip link, foco visible, aria-expanded y aria-live

1. Texto alternativo ​

Ya lo viste en Imágenes: todo <img> lleva alt; descriptivo si aporta información, vacío (alt="") si es decorativo. Los SVG inline informativos necesitan un título:

html
<svg role="img" aria-labelledby="titulo-icono" width="24" height="24">
  <title id="titulo-icono">Envío gratis</title>
  <!-- trazos -->
</svg>

<!-- Icono decorativo junto a un texto: ocúltalo -->
<button><svg aria-hidden="true" focusable="false">...</svg> Agregar al carrito</button>

2. Contraste de color ​

El texto debe distinguirse bien del fondo. WCAG AA pide:

  • 4.5:1 para texto normal.
  • 3:1 para texto grande (24px, o 18.66px en negrita) y para componentes de interfaz (bordes de campos, iconos).

Herramientas: el inspector de Chrome muestra el contraste al seleccionar un color; también WebAIM Contrast Checker.

Error común

Transmitir información solo con color: "los campos en rojo son obligatorios". Una persona daltónica no lo notará. Añade texto o un icono: "Obligatorio *", "Error: el correo no es válido".

3. Navegación por teclado y orden del foco ​

Todo lo que se puede hacer con el mouse debe poder hacerse con el teclado.

html
<!-- ✅ Elementos nativos: enfocables y activables con teclado -->
<button type="button">Abrir menú</button>
<a href="/contacto">Contacto</a>

<!-- ❌ Un div no recibe foco ni responde a Enter/Espacio -->
<div class="boton" onclick="abrirMenu()">Abrir menú</div>

El orden del foco sigue el orden del HTML. Si con CSS reordenas visualmente los elementos (con order en Flexbox o posiciones en Grid), el orden del teclado puede volverse caótico. Mantén el orden del código igual al orden visual.

tabindex ​

html
<!-- 0: hace enfocable un elemento que normalmente no lo es (en su orden natural) -->
<div tabindex="0" role="region" aria-label="Tabla de precios con scroll">...</div>

<!-- -1: enfocable solo con JavaScript (por ejemplo, el título al abrir un modal) -->
<h2 tabindex="-1" id="resultado">Resultados</h2>

<!-- ❌ Valores positivos: rompen el orden natural. Nunca los uses -->
<input tabindex="3">

Foco visible ​

css
/* ❌ Nunca hagas esto sin dar una alternativa */
*:focus { outline: none; }

/* ✅ Foco claro solo cuando se navega con teclado */
:focus-visible {
  outline: 3px solid #f59e0b;
  outline-offset: 2px;
}

4. Labels en formularios ​

Todo campo necesita un nombre accesible. Repasa Formularios. Para textos de ayuda y errores, vincúlalos con aria-describedby:

html
<label for="clave">Contraseña</label>
<input id="clave" type="password" aria-describedby="clave-ayuda clave-error" aria-invalid="true">
<p id="clave-ayuda">Mínimo 8 caracteres.</p>
<p id="clave-error">La contraseña es demasiado corta.</p>

El lector anunciará: "Contraseña, campo de contraseña, no válido, Mínimo 8 caracteres. La contraseña es demasiado corta."

5. Landmarks (regiones) ​

Los elementos semánticos crean regiones a las que se puede saltar directamente:

ElementoRegión (rol)
<header> (hijo de body)banner
<nav>navigation
<main>main
<aside>complementary
<footer> (hijo de body)contentinfo
<section> con nombre accesibleregion
<form> con nombre accesibleform
<search> o role="search"search

Por eso es tan importante el HTML semántico.

Un usuario de teclado tendría que pasar por los 15 enlaces del menú en cada página antes de llegar al contenido. El skip link es un enlace oculto que aparece con el primer Tab:

html
<body>
  <a class="saltar" href="#contenido">Saltar al contenido principal</a>
  <header>...menú largo...</header>
  <main id="contenido">...</main>
</body>
css
.saltar {
  position: absolute;
  left: 8px;
  top: -60px;          /* fuera de la vista */
  padding: 8px 14px;
  background: #1e293b;
  color: #fff;
  z-index: 100;
}
.saltar:focus { top: 8px; } /* aparece al recibir foco */

7. ARIA ​

ARIA (Accessible Rich Internet Applications) son atributos que añaden información para tecnologías de asistencia cuando el HTML no alcanza: componentes interactivos hechos con JavaScript (pestañas, menús desplegables, notificaciones).

La primera regla de ARIA

No uses ARIA si existe un elemento HTML nativo que haga lo mismo. Un <button> es mejor que un <div role="button">. "Ningún ARIA es mejor que un ARIA mal usado": los sitios con ARIA suelen tener más errores de accesibilidad que los que no lo usan.

Roles ​

Indican qué es un elemento:

html
<div role="alert">Tu sesión expirará en 1 minuto.</div>
<div role="tablist">
  <button role="tab" aria-selected="true">Descripción</button>
  <button role="tab" aria-selected="false">Reseñas</button>
</div>
<ul role="list">...</ul> <!-- restaura la semántica de lista si CSS la quitó en Safari -->

aria-label y aria-labelledby ​

Dan un nombre accesible:

html
<!-- Botón solo con icono: sin aria-label el lector diría solo "botón" -->
<button type="button" aria-label="Cerrar">✕</button>
<button type="button" aria-label="Buscar"><svg aria-hidden="true">...</svg></button>

<!-- Distinguir dos regiones de navegación -->
<nav aria-label="Principal">...</nav>
<nav aria-label="Pie de página">...</nav>

<!-- Usar un texto visible existente como nombre -->
<section aria-labelledby="titulo-planes">
  <h2 id="titulo-planes">Planes y precios</h2>
</section>

aria-expanded y aria-controls ​

Indican si un elemento que se despliega está abierto:

html
<button type="button" aria-expanded="false" aria-controls="menu-movil" id="btn-menu">
  Menú
</button>
<ul id="menu-movil" hidden>...</ul>
js
const btn = document.getElementById('btn-menu');
const menu = document.getElementById('menu-movil');
btn.addEventListener('click', () => {
  const abierto = btn.getAttribute('aria-expanded') === 'true';
  btn.setAttribute('aria-expanded', String(!abierto)); // actualiza el estado
  menu.hidden = abierto;
});

El lector dirá "Menú, botón, contraído" y luego "expandido". Bonus: en CSS puedes estilizar según el estado con [aria-expanded="true"].

aria-live: anunciar cambios ​

Cuando el contenido cambia sin recargar (carrito, mensajes, resultados de búsqueda), el lector no se entera… a menos que uses una región viva:

html
<!-- polite: anuncia cuando el usuario termine lo que está haciendo -->
<p aria-live="polite" id="estado-carrito">Carrito: 0 productos</p>

<!-- assertive o role="alert": interrumpe (solo para errores urgentes) -->
<div role="alert" id="error"></div>

<!-- role="status" equivale a aria-live="polite" -->
<p role="status">Formulario enviado con éxito.</p>

La región debe existir en el HTML desde el inicio; luego cambias su texto con JavaScript.

Otros atributos frecuentes ​

AtributoUso
aria-hidden="true"Oculta al lector (iconos decorativos). Nunca en elementos enfocables.
aria-current="page"Página actual en un menú.
aria-describedbyAsocia una descripción (ayuda, error).
aria-invalid="true"Campo con error.
aria-required="true"Obligatorio (si no puedes usar required).
aria-pressedBotón de alternar (activo/inactivo).
aria-modal="true"Diálogo modal (<dialog> con showModal() ya lo hace).

Texto solo para lectores de pantalla ​

css
/* Oculta visualmente pero mantiene el texto para lectores */
.solo-lector {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
html
<a href="/blog/cafe-de-olla">Leer más <span class="solo-lector">sobre café de olla</span></a>

8. Otros detalles importantes ​

  • Idioma: <html lang="es"> (ya lo viste).
  • Títulos de página únicos y descriptivos.
  • Encabezados en orden lógico.
  • Áreas táctiles de al menos 44×44 px en botones y enlaces en móvil.
  • Movimiento: respeta prefers-reduced-motion en animaciones.
  • No dependas del hover: en celular no existe.
  • Zoom: el sitio debe funcionar al 200% sin perder contenido.

Cómo probar la accesibilidad ​

  1. Teclado (5 minutos, lo más revelador): desconecta el mouse y recorre la página con Tab. ¿Ves siempre dónde está el foco? ¿Puedes abrir el menú, llenar el formulario y cerrar los modales?
  2. Lighthouse: en Chrome, F12 → pestaña Lighthouse → marca Accessibility → Analyze. Apunta a 95–100. Corrige todo lo que marque.
  3. Extensiones: axe DevTools o WAVE detectan problemas y explican cómo resolverlos.
  4. Lector de pantalla:
    • Windows: instala NVDA (gratis). Tecla H salta entre encabezados, D entre regiones, K entre enlaces, F entre campos.
    • Mac: VoiceOver con Cmd+F5. El rotor (Ctrl+Opción+U) muestra encabezados, enlaces y regiones.
    • Android/iPhone: TalkBack o VoiceOver desde Accesibilidad.
  5. Zoom: Ctrl + al 200% y revisa que nada se corte ni se encime.
  6. Panel de accesibilidad del inspector de Chrome: muestra el "árbol de accesibilidad" y el nombre accesible de cada elemento.

Buena práctica

Las herramientas automáticas detectan solo alrededor del 30–40% de los problemas. Un 100 en Lighthouse no garantiza que el sitio sea accesible: la prueba con teclado y con lector de pantalla es imprescindible.

Casos de uso reales ​

  • Menú hamburguesa: <button> con aria-expanded, aria-controls y aria-label="Abrir menú".
  • Carrito de compras: contador con aria-live="polite".
  • Formulario de registro: errores vinculados con aria-describedby y aria-invalid.
  • Iconos de redes sociales en el pie: aria-label="Instagram de Café Aroma" en cada enlace.
  • Sitio de gobierno o universidad: cumplimiento de WCAG 2.2 AA como requisito del contrato.

Ejercicios ​

  1. Corrige este botón para que sea accesible: <div class="btn" onclick="cerrar()">✕</div>.
  2. Crea un skip link que lleve a <main id="principal">.
  3. Crea un botón "Ver filtros" que muestre u oculte un panel, comunicando su estado a lectores de pantalla.
  4. Haz una prueba con Lighthouse de cualquier sitio que hayas creado y anota los tres primeros problemas.
Ver solución
html
<!-- Ejercicio 1 -->
<button type="button" class="btn" aria-label="Cerrar" onclick="cerrar()">
  <span aria-hidden="true">✕</span>
</button>

<!-- Ejercicio 2 -->
<a class="saltar" href="#principal">Saltar al contenido principal</a>
<!-- ... -->
<main id="principal">...</main>

<!-- Ejercicio 3 -->
<button type="button" id="btn-filtros" aria-expanded="false" aria-controls="filtros">
  Ver filtros
</button>
<div id="filtros" hidden>
  <!-- checkboxes de filtros -->
</div>
js
const btn = document.getElementById('btn-filtros');
const panel = document.getElementById('filtros');
btn.addEventListener('click', () => {
  const abierto = btn.getAttribute('aria-expanded') === 'true';
  btn.setAttribute('aria-expanded', String(!abierto));
  panel.hidden = abierto;
});

Resumen ​

  • La accesibilidad beneficia a todos y en muchos casos es obligación legal; el objetivo habitual es WCAG AA.
  • HTML semántico resuelve la mayoría de los problemas: botones reales, labels, landmarks, encabezados.
  • Contraste mínimo 4.5:1; no transmitas información solo con color.
  • Todo debe funcionar con teclado y con foco visible (:focus-visible); evita tabindex positivos.
  • Usa skip links para saltar el menú.
  • ARIA solo cuando el HTML no alcanza: aria-label, aria-expanded, aria-live, aria-describedby.
  • Prueba con teclado, Lighthouse, axe/WAVE y un lector de pantalla real.

Academia Web — aprende, copia y construye.