Lección 6 de 10 · 14 min de lectura · Revisado el 26/10/2026
JavaScript accesible: eventos, cambios de contexto, límites de tiempo y contenido dinámico
El HTML aporta la semántica y el CSS la presentación; el JavaScript decide qué ocurre y cuándo. Ahí aparecen problemas que ninguna etiqueta corrige: acciones que se disparan al presionar en lugar de al soltar, desplegables que cambian de página sin avisar, sesiones que caducan en mitad de un trámite y contenido que se sustituye llevándose el foco por delante. Esta lección reúne los criterios que dependen del comportamiento programado (2.5.2 Cancelación del puntero, 3.2.1 Al recibir el foco, 3.2.2 Al recibir entradas y 2.2.1 Tiempo ajustable) y las técnicas para gestionar el contenido que aparece y desaparece. Es la base de la lección siguiente, en la que verás cómo se aplica todo esto a las aplicaciones de una sola página.
Eventos: escucha la intención, no el dispositivo
El evento click de un elemento nativo es el más inclusivo que existe: se dispara con el ratón, con un toque en la pantalla, con Intro o la barra espaciadora, con el control por voz, con un pulsador y con los gestos de activación de los lectores de pantalla. Por eso un <button> con un manejador click no necesita nada más para ser operable.
Los eventos de bajo nivel (mousedown, pointerdown, touchstart) son otra historia. 2.5.2 Cancelación del puntero (A) exige que, para la funcionalidad que se maneja con un solo puntero, se cumpla al menos una de estas condiciones: que no se ejecute nada al presionar (evento descendente); que la acción se complete al soltar y se pueda abortar apartando el puntero antes de soltar, o deshacer después; que al soltar se revierta lo que hizo el presionar; o que actuar al presionar sea esencial (las teclas de un piano virtual). Se trata de poder arrepentirse, algo vital para quien tiene temblores.
Ejemplo
// Incorrecto: borra en cuanto se presiona, sin posibilidad de cancelar,
// y no funciona con teclado
filaBorrar.addEventListener('mousedown', () => eliminarExpediente(id));
// Correcto: click se dispara al soltar (y con teclado); si el puntero
// sale del botón antes de soltar, no ocurre nada
botonBorrar.addEventListener('click', () => eliminarExpediente(id));
El arrastrar y soltar es el caso legítimo de presionar para «coger»: cumple porque la acción se completa al soltar y se cancela soltando fuera del destino. Aun así necesita una alternativa sin arrastre, como viste con 2.5.7 Movimientos de arrastre en el curso de componentes.
Otras reglas para los manejadores de eventos:
- Si programas teclado para un control personalizado, usa
keydowny la propiedadevent.key('Enter',' ','Escape','ArrowDown');keypressykeyCodeestán obsoletos. - No captures teclas de forma global sin necesidad: los lectores de pantalla y las extensiones usan muchas. Si añades atajos de una sola tecla, deben poder desactivarse o reasignarse, o funcionar solo con el foco en el componente (2.1.4 Atajos de teclado, visto en el curso de teclado).
- Todo lo que hagas con
mouseenteromouseoverdebe tener su equivalente confocusofocusin.
Cambios de contexto: 3.2.1 y 3.2.2
Las WCAG distinguen entre cambio de contenido y cambio de contexto. Un cambio de contexto es un cambio importante que, si se produce sin que la persona lo espere, la desorienta: abrir una ventana nueva, navegar a otra página, mover el foco a otro sitio o cambiar el contenido de forma que cambie el sentido de la página. Filtrar una lista de resultados dentro de la misma página es un cambio de contenido; llevarte a otra página, un cambio de contexto.
- 3.2.1 Al recibir el foco (A): que un elemento reciba el foco no debe provocar un cambio de contexto. Fallos típicos: abrir una ventana emergente al enfocar un campo, enviar un formulario al llegar a un botón, saltar el foco a otro sitio al tabular.
- 3.2.2 Al recibir entradas (A): cambiar el valor de un control (elegir una opción, marcar una casilla, escribir) no debe provocar automáticamente un cambio de contexto, salvo que se haya avisado antes.
Error frecuente
<!-- Incorrecto: elegir una opción navega de inmediato -->
<label for="idioma">Idioma</label>
<select id="idioma" onchange="location.href = this.value">…</select>
<!-- Correcto: la persona elige y confirma -->
<form action="/cambiar-idioma">
<label for="idioma">Idioma</label>
<select id="idioma" name="idioma">…</select>
<button type="submit">Cambiar idioma</button>
</form>
En muchos navegadores, con teclado, al recorrer las opciones de un select cerrado con las flechas cada opción dispara change: quien quería pasar de «Castellano» a «Valencià» acaba en «Català» sin haberlo elegido. Lo mismo pasa con el formulario que se envía solo al escribir el último dígito de un código.
Avisar antes («Al elegir una provincia se cargará la página de su delegación») hace que se cumpla el criterio, pero la solución más sólida es que la persona confirme con un botón. Los cambios de contenido sí pueden ser inmediatos; solo hay que comunicarlos, por ejemplo escribiendo «23 resultados» en una región role="status", como viste en el curso de ARIA.
Límites de tiempo: sesiones que caducan
Las sedes electrónicas, la banca en línea y las pasarelas de pago cierran la sesión por inactividad. Es legítimo, pero 2.2.1 Tiempo ajustable (A) exige que, para cada límite de tiempo, se cumpla al menos una opción: que la persona pueda desactivarlo; que pueda ajustarlo a por lo menos diez veces su duración; o que reciba un aviso antes de que expire, disponga de al menos 20 segundos para ampliarlo con una acción sencilla (por ejemplo, pulsar una tecla) y pueda ampliarlo al menos diez veces. Quedan exceptuados los eventos en tiempo real (una subasta), los límites esenciales (un examen cronometrado) y los que superan las 20 horas.
Quien usa un pulsador o un lector de pantalla, o necesita más tiempo para leer, puede tardar varias veces más. Una implementación para una sesión de 15 minutos:
const DURACION = 15 * 60 * 1000;
const AVISO_ANTES = 2 * 60 * 1000;
let temporizador;
function programarAviso() {
clearTimeout(temporizador);
temporizador = setTimeout(mostrarAviso, DURACION - AVISO_ANTES);
}
function mostrarAviso() {
// Diálogo modal (role="alertdialog") con el foco en «Ampliar sesión»
dialogoSesion.showModal();
botonAmpliar.focus();
}
botonAmpliar.addEventListener('click', async () => {
await renovarSesionEnServidor();
dialogoSesion.close();
programarAviso();
});
Dos complementos AAA que marcan la diferencia en trámites largos: 2.2.5 Reautenticación (si la sesión caduca, al volver a identificarse se continúa sin perder datos) y 2.2.6 Tiempos de espera (avisar de cuánta inactividad provoca la pérdida de datos, salvo que se conserven más de 20 horas). Guardar un borrador del formulario ayuda a cumplir ambos y evita perder media hora de trabajo.
Contenido que aparece, desaparece o se sustituye
Cada vez que el JavaScript modifica el DOM hay que preguntarse dos cosas: dónde queda el foco y cómo se entera quien no ve la pantalla.
- Inserta el contenido nuevo justo después de su disparador en el DOM: el panel de un acordeón, el texto de «Mostrar más», los campos que aparecen al marcar una opción. Así el siguiente Tab llega a él. Si lo añades al final del
body, nadie lo encontrará. - Botón «Cargar más»: tras cargar, mueve el foco al primer elemento nuevo o deja el foco en el botón y anuncia «10 resultados más cargados» en una región de estado.
- Elemento eliminado: si el elemento que tenía el foco desaparece (la fila que se acaba de borrar, el botón que se sustituye por otro), el foco cae al
bodyy la persona vuelve al principio de la página. Mueve el foco a un sitio lógico antes de eliminarlo: el siguiente elemento de la lista, el anterior si era el último o el encabezado de la lista si quedó vacía. - Contenido que debe quedar fuera de alcance: el atributo
inert, soportado en todos los navegadores actuales, hace que un bloque no sea enfocable ni llegue a las tecnologías de apoyo. Es ideal para un menú lateral cerrado que se anima fuera de pantalla (dondedisplay: noneimpediría la transición) o para el resto de la página mientras se muestra una capa propia. El<dialog>nativo abierto conshowModal()ya vuelve inerte el resto por sí mismo.
function alternarMenu(abierto) {
menuLateral.inert = !abierto;
menuLateral.classList.toggle('abierto', abierto);
botonMenu.setAttribute('aria-expanded', String(abierto));
if (abierto) menuLateral.querySelector('a, button')?.focus();
else botonMenu.focus();
}
Para los anuncios que no implican mover el foco, usa regiones activas presentes desde la carga y espera a que la persona deje de escribir antes de anunciar resultados de una búsqueda en vivo (unos 500 milisegundos), para no interrumpir cada pulsación.
Pruébalo tú
Abre la consola de las herramientas de desarrollo en una aplicación web con listas editables y escribe: document.addEventListener('focusin', () => console.log(document.activeElement)). Navega con el teclado, borra un elemento o cierra un panel, y mira la consola. Después escribe document.activeElement: si responde body, el foco se ha perdido y has encontrado un fallo.
Punto clave
Programa acciones con click en elementos nativos, no cambies de contexto al enfocar ni al cambiar un valor, avisa y deja ampliar cualquier límite de tiempo y, cada vez que el DOM cambie, decide dónde queda el foco y cómo se comunica el cambio.
Resumen
- Usa
clicken elementos nativos; ejecutar acciones enmousedownopointerdownincumple 2.5.2 salvo que se puedan cancelar o sea esencial. - Enfocar un elemento no puede cambiar el contexto (3.2.1) y cambiar un valor tampoco sin aviso previo (3.2.2): añade un botón de confirmación.
- Toda sesión que caduca debe avisar antes y permitir ampliar con al menos 20 segundos y una acción simple (2.2.1); conserva los datos.
- Inserta el contenido tras su disparador, recoloca el foco antes de eliminar elementos y usa
inertpara lo que debe quedar fuera de alcance.
Comprueba lo que has aprendido
Esta lección se puede leer libremente. Para hacer su test de 3 preguntas, los ejercicios prácticos con páginas reales, guardar tu avance y optar al examen y al certificado, matricúlate en el curso.