En la lección anterior construimos widgets y, una y otra vez, remitimos "a 05-02" para las mecánicas de teclado y foco. Ha llegado el momento. El teclado es la capa base de toda la accesibilidad interactiva: si algo funciona con teclado, casi siempre funciona también con un lector de pantalla, un conmutador o un control por voz. Lucía navega exclusivamente con teclado por su movilidad reducida; Marta usa NVDA, que se apoya en el modelo de foco del navegador. Si tu componente no es operable con teclado, para ellas simplemente no existe. En esta lección cubrimos los fundamentos: qué es focusable, cómo funciona tabindex, cómo mover el foco sin robarlo, cómo evitar las trampas de teclado, y dos técnicas clave —roving tabindex y focus trap— más la tabla de referencia de teclas que el resto del curso cita.
Contenido
- Qué elementos son focusables de forma nativa
tabindex: 0, -1 y por qué nunca positivo- El orden de foco es el orden del DOM
- Foco visible y foco programático (
element.focus()) - Trampas de teclado (criterio 2.1.2) y cómo evitarlas
- Skip links que funcionan de verdad
- Técnica: roving tabindex
- Técnica: focus trap para diálogos modales
- Tabla exhaustiva de convenciones de teclas
- Errores comunes, ejercicios y conclusión
- Qué elementos son focusables de forma nativa
Solo un conjunto reducido de elementos HTML puede recibir el foco de teclado sin ayuda:
| Elemento | Focusable por defecto | Nota |
|---|---|---|
<a href="…"> |
Sí | Solo si tiene href |
<button> |
Sí | También responde a Enter/Espacio |
<input>, <select>, <textarea> |
Sí | Salvo disabled |
<div>, <span>, <p> |
No | Necesitan tabindex |
Elemento con tabindex="0" |
Sí | Añadido manualmente |
Elemento con contenteditable |
Sí | — |
La lección aquí es contundente: usa elementos nativos. Un <button> es focusable, operable con Enter y Espacio, anunciado como botón y compatible con todas las tecnologías de asistencia sin escribir una línea de JavaScript. En cambio, un <div role="button"> te obliga a añadir tabindex="0", gestionar Enter y Espacio a mano y aun así arriesgas incompatibilidades. La primera regla de ARIA del Módulo 4 —"prefiere HTML nativo"— es, en el fondo, una regla de teclado.
tabindex: 0, -1 y por qué nunca positivo
tabindex: 0, -1 y por qué nunca positivotabindex controla si un elemento entra en el orden de tabulación y su posición. Tiene tres usos, y solo dos son buenos:
<!-- tabindex="0": entra en el orden de tab en su posición natural del DOM -->
<div role="button" tabindex="0">Botón personalizado</div>
<!-- tabindex="-1": NO entra en el orden de tab, pero SÍ es focusable por JS -->
<div id="panel" tabindex="-1">Panel al que llevo el foco con .focus()</div>
<!-- tabindex="5": NUNCA. Rompe el orden natural. -->
<input tabindex="5">tabindex="0": hace focusable un elemento que no lo es, respetando el orden del DOM. Úsalo para widgets personalizados.tabindex="-1": quita el elemento del recorrido deTab, pero permite enfocarlo por programación conelement.focus(). Es imprescindible para llevar el foco a un panel, a un encabezado tras navegar, o para el roving tabindex.tabindexpositivo (1, 2, 3…): nunca. Un valor positivo salta por delante de todo el orden natural, creando un recorrido de foco impredecible que es imposible de mantener. Basta untabindex="1"perdido para desordenar toda la página. Si necesitas cambiar el orden, reordena el HTML.
- El orden de foco es el orden del DOM
Cuando el usuario pulsa Tab, el foco avanza siguiendo el orden del código HTML, no el orden visual de la pantalla. Esto tiene una consecuencia práctica enorme: si usas CSS (order de flexbox, grid, position) para reordenar visualmente los elementos, el foco seguirá el orden del DOM y no coincidirá con lo que se ve.
/* PELIGRO: el segundo elemento se ve primero, pero el foco lo visita después */
.barra { display: flex; }
.barra .accion-principal { order: -1; } /* salta al inicio visualmente */Lucía verá el botón principal a la izquierda, pero al tabular el foco irá a otro sitio primero. El criterio WCAG 2.4.3 (Orden del foco) exige que el orden sea lógico y significativo. La regla segura: mantén el orden del DOM coherente con el orden de lectura visual y usa CSS para el aspecto, no para reordenar el flujo de interacción.
- Foco visible y foco programático
Dos caras del mismo tema:
Foco visible. El usuario de teclado debe ver dónde está el foco en todo momento (criterio 2.4.7). El error histórico más grave de la web es outline: none sin reemplazo. Como vimos en el Módulo 4, la solución moderna es :focus-visible:
/* Correcto: anillo de foco claro, solo cuando se navega con teclado */
:focus-visible {
outline: 3px solid #1a73e8;
outline-offset: 2px;
}
/* NUNCA dejes esto sin una alternativa visible: */
/* button:focus { outline: none; } ❌ */Foco programático. Con element.focus() movemos el foco desde JavaScript. Es la herramienta que usan todos los widgets del módulo. Reglas de oro:
// Correcto: mover el foco como RESULTADO de una acción del usuario
btnAbrir.addEventListener('click', () => {
modal.hidden = false;
modal.querySelector('input').focus(); // el usuario pidió abrir → movemos foco
});- Nunca robes el foco sin que el usuario lo haya pedido. Mover el foco durante el scroll, en un
setInterval, o al cargar un anuncio, desorienta y puede violar el criterio 3.2.1 (Al recibir el foco) y 3.2.5. El foco se mueve como respuesta a una acción, no por sorpresa. - Para enfocar un contenedor no interactivo (un panel, un
<h2>de una sección nueva) daletabindex="-1"primero, o el.focus()no tendrá efecto.
- Trampas de teclado (criterio 2.1.2) y cómo evitarlas
Una trampa de teclado ocurre cuando el foco entra en un componente y no puede salir con el teclado. El usuario queda atrapado y, sin ratón, no tiene escapatoria. El criterio 2.1.2 (Sin trampas para el foco del teclado) es de nivel A: es inaceptable. Causas típicas:
- Un widget de terceros (un editor de texto enriquecido, un mapa) que captura
Taby nunca lo suelta. - Un manejador de teclado que hace
event.preventDefault()enTabsin reenviar el foco a otro sitio.
// TRAMPA: se cancela Tab pero no se mueve el foco a ningún lado → atrapado
editor.addEventListener('keydown', (e) => {
if (e.key === 'Tab') e.preventDefault(); // ❌ el usuario no puede salir
});Ojo: un focus trap de modal es distinto y es legítimo, porque el usuario siempre puede salir con Esc (lo vemos en el punto 8). La diferencia entre una trampa prohibida y un trap correcto es que el trap siempre ofrece una vía de escape estándar.
- Skip links que funcionan de verdad
Un skip link ("Saltar al contenido principal") permite a Lucía y a Marta evitar tabular por toda la cabecera y el menú en cada página. Es un ancla que debe mover el foco de verdad, no solo hacer scroll:
<a href="#main" class="skip-link">Saltar al contenido principal</a>
<!-- … cabecera y navegación … -->
<main id="main" tabindex="-1">…</main>/* Oculto hasta que recibe el foco: aparece al tabular */
.skip-link {
position: absolute;
left: -9999px;
}
.skip-link:focus {
left: 1rem;
top: 1rem;
}El detalle crítico es el tabindex="-1" en el <main>: sin él, en algunos navegadores el enlace hace scroll pero el foco se queda arriba, y la siguiente pulsación de Tab vuelve al menú. Con tabindex="-1", al activar el enlace el foco aterriza dentro de <main> y la navegación continúa desde ahí.
- Técnica: roving tabindex
En un grupo de elementos relacionados (pestañas, barra de herramientas, grupo de radios personalizado) no queremos que Tab visite cada uno: sería agotador. La APG especifica que Tab entre al grupo por una sola parada y que las flechas muevan dentro. Eso se implementa con roving tabindex: en todo momento, solo un elemento del grupo tiene tabindex="0" y el resto tabindex="-1"; al moverse con las flechas, "rodamos" ese 0.
const items = [...document.querySelectorAll('[role="tab"]')];
let indice = 0;
function mover(nuevoIndice) {
items[indice].tabIndex = -1; // el anterior sale del tab
indice = (nuevoIndice + items.length) % items.length; // envolvente
items[indice].tabIndex = 0; // el nuevo entra en el tab
items[indice].focus(); // y recibe el foco
}
items.forEach((item, i) => {
item.tabIndex = i === 0 ? 0 : -1; // estado inicial
item.addEventListener('keydown', (e) => {
if (e.key === 'ArrowRight') { mover(indice + 1); e.preventDefault(); }
if (e.key === 'ArrowLeft') { mover(indice - 1); e.preventDefault(); }
if (e.key === 'Home') { mover(0); e.preventDefault(); }
if (e.key === 'End') { mover(items.length - 1); e.preventDefault(); }
});
});Así, Lucía pulsa Tab una vez para entrar en las pestañas del panel de Cursalia y luego usa las flechas para recorrerlas, exactamente como espera un usuario de lector de pantalla.
- Técnica: focus trap para diálogos modales
Cuando la matrícula (el modal de 05-01) está abierta, el foco debe ciclar dentro del diálogo: al llegar al último elemento y pulsar Tab, volvemos al primero; con Shift+Tab en el primero, saltamos al último. Y Esc cierra. Implementación completa:
function activarTrap(modal, disparador) {
// Todos los elementos focusables DENTRO del modal
const focusables = modal.querySelectorAll(
'a[href], button:not([disabled]), input:not([disabled]), select, textarea, [tabindex]:not([tabindex="-1"])'
);
const primero = focusables[0];
const ultimo = focusables[focusables.length - 1];
primero.focus(); // foco inicial dentro del diálogo
function alPulsar(e) {
if (e.key === 'Escape') {
cerrar();
return;
}
if (e.key !== 'Tab') return;
// Ciclado del foco: aquí está el "trap"
if (e.shiftKey && document.activeElement === primero) {
ultimo.focus(); // Shift+Tab en el primero → al último
e.preventDefault();
} else if (!e.shiftKey && document.activeElement === ultimo) {
primero.focus(); // Tab en el último → al primero
e.preventDefault();
}
}
function cerrar() {
modal.hidden = true;
modal.removeEventListener('keydown', alPulsar);
disparador.focus(); // DEVOLVER el foco a quien abrió (imprescindible)
}
modal.addEventListener('keydown', alPulsar);
return cerrar;
}Este trap sí es correcto porque Esc siempre ofrece salida: no viola el 2.1.2. Nota moderna: el elemento nativo <dialog> con showModal() implementa gran parte de esto por ti (foco atrapado y Esc), así que prefiérelo cuando puedas y reserva este código para navegadores o casos que lo requieran.
- Tabla exhaustiva de convenciones de teclas
Esta es la tabla de referencia que 05-01 y el Módulo 7 citan. Sigue las convenciones de la APG por tipo de widget:
| Widget | Tecla(s) | Acción esperada |
|---|---|---|
| Botón | Enter, Espacio |
Activar |
| Enlace | Enter |
Seguir el enlace (el Espacio NO lo activa; hace scroll) |
| Checkbox | Espacio |
Marcar/desmarcar |
| Radio (grupo) | ↑ ↓ ← → |
Mover y seleccionar dentro del grupo; Tab entra/sale del grupo |
| Menú / menú botón | Enter/Espacio/↓ abre; ↑ ↓ mueve; Esc cierra; Home/End extremos |
Navegar opciones |
| Tabs (pestañas) | ← → (o ↑ ↓ si vertical) mueve; Home/End primera/última; Tab sale al panel |
Cambiar de pestaña |
| Combobox / listbox | ↓/↑ abre y mueve; Enter selecciona; Esc cierra; escribir filtra |
Elegir opción del filtro |
| Acordeón / disclosure | Enter, Espacio |
Expandir/colapsar la sección |
| Diálogo modal | Esc cierra; Tab/Shift+Tab ciclan dentro (focus trap) |
Operar y cerrar |
| Slider (deslizador) | ← → / ↑ ↓ paso; Home/End mín/máx; AvPág/RePág salto grande |
Ajustar valor |
Regla mnemotécnica: Enter/Espacio activan, Esc cancela/cierra, las flechas navegan dentro de un grupo, Home/End van a los extremos. Si respetas estas convenciones, Marta y Lucía se sienten en casa porque coinciden con las de las aplicaciones de escritorio.
Errores Comunes y Consejos
onclicken un<div>sin soporte de teclado. El clic funciona con ratón pero Lucía no puede activarlo. Usa<button>o, si no hay más remedio, añadetabindex="0"y manejaEnteryEspacio.tabindexpositivo. Rompe el orden de foco de toda la página. Prohibido.outline: nonesin alternativa. Deja a los usuarios de teclado sin saber dónde están. Usa:focus-visible.- Cancelar
Tabsin mover el foco. Crea una trampa (2.1.2). Si interceptasTab, siempre reenvía el foco a algún sitio. - Robar el foco. Mover el foco sin que el usuario lo haya pedido (en scroll, temporizadores, carga de anuncios) es desorientador y viola 3.2.x.
- Consejo: prueba cada página desenchufando el ratón. Si puedes llegar a todo, activar todo y ver siempre dónde estás, has ganado la mitad de la batalla de accesibilidad.
Ejercicios
Ejercicio 1. Explica la diferencia entre tabindex="0" y tabindex="-1" y da un caso de uso real para cada uno.
Ejercicio 2. El siguiente skip link hace scroll pero el foco vuelve al menú en la siguiente pulsación de Tab. ¿Qué falta?
Ejercicio 3. Tienes una barra de herramientas con cinco botones y quieres que Tab entre por una sola parada y las flechas muevan entre ellos. ¿Qué técnica usas y cuál es su idea central en una frase?
Soluciones
Solución 1. tabindex="0" mete un elemento no focusable en el orden natural de tabulación (caso: un <div role="button"> personalizado que debe ser alcanzable con Tab). tabindex="-1" lo saca del orden de Tab pero permite enfocarlo por JavaScript con .focus() (caso: un panel o un <main> al que llevas el foco tras una acción, o los elementos inactivos de un roving tabindex).
Solución 2. Falta tabindex="-1" en el <main>. Sin él, el ancla mueve el scroll pero no el foco, así que el foco permanece en la cabecera y Tab vuelve al menú. Con <main id="main" tabindex="-1">, al activar el enlace el foco aterriza en el contenido principal.
Solución 3. Se usa roving tabindex: solo el botón "activo" tiene tabindex="0" y los demás tabindex="-1"; al pulsar las flechas se traslada ese 0 (y el foco) al siguiente botón. Idea central en una frase: una sola parada de tabulación para todo el grupo; las flechas mueven el foco dentro.
Conclusión
Ya dominas la capa base: qué es focusable, cómo se comporta tabindex, por qué el orden de foco sigue al DOM, cómo mover el foco sin robarlo, cómo evitar trampas de teclado, y las dos técnicas que sostienen los widgets del módulo —roving tabindex y focus trap—, además de la tabla de teclas de referencia. Con esto, los componentes de 05-01 quedan completamente operables sin ratón. Hemos hecho accesible la interacción; nos falta hacer accesible el contenido multimedia. En la siguiente lección, 05-03: Contenido de Video y Audio Accesible, implementaremos subtítulos, transcripciones y audiodescripción sobre el reproductor de vídeo-lección de Cursalia, para Diego, Marta y Hugo.
Curso de Accesibilidad Web
Módulo 1: Introducción a la Accesibilidad Web
- ¿Qué es la Accesibilidad Web?
- Importancia de la Accesibilidad Web
- Visión General de las Leyes y Normas de Accesibilidad
- Introducción a WCAG
Módulo 2: Comprensión de Discapacidades y Tecnologías de Asistencia
- Tipos de Discapacidades
- Visión General de Tecnologías de Asistencia
- Cómo las Personas con Discapacidades Usan la Web
Módulo 3: Principios del Diseño Accesible
- Perceptible: Hacer el Contenido Disponible a los Sentidos
- Operable: Interfaz de Usuario y Navegación
- Comprensible: Información y Operación
- Robusto: Compatibilidad con Tecnologías Actuales y Futuras
Módulo 4: Implementación de Accesibilidad en HTML y CSS
- HTML Semántico
- Formularios Accesibles
- Uso de Roles y Propiedades ARIA
- Contraste de Color y Redimensionamiento de Texto
Módulo 5: Accesibilidad en JavaScript y Multimedia
- Creación de Widgets de JavaScript Accesibles
- Accesibilidad del Teclado
- Contenido de Video y Audio Accesible
- Proporcionar Alternativas de Texto para Imágenes
Módulo 6: Pruebas y Evaluación de la Accesibilidad
- Técnicas de Pruebas Manuales
- Herramientas de Pruebas Automatizadas
- Pruebas de Usuario con Tecnologías de Asistencia
- Interpretación de Informes de Accesibilidad
