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

  1. Qué elementos son focusables de forma nativa
  2. tabindex: 0, -1 y por qué nunca positivo
  3. El orden de foco es el orden del DOM
  4. Foco visible y foco programático (element.focus())
  5. Trampas de teclado (criterio 2.1.2) y cómo evitarlas
  6. Skip links que funcionan de verdad
  7. Técnica: roving tabindex
  8. Técnica: focus trap para diálogos modales
  9. Tabla exhaustiva de convenciones de teclas
  10. Errores comunes, ejercicios y conclusión

  1. 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="…"> Solo si tiene href
<button> También responde a Enter/Espacio
<input>, <select>, <textarea> Salvo disabled
<div>, <span>, <p> No Necesitan tabindex
Elemento con tabindex="0" Añadido manualmente
Elemento con contenteditable

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.

  1. tabindex: 0, -1 y por qué nunca positivo

tabindex 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 de Tab, pero permite enfocarlo por programación con element.focus(). Es imprescindible para llevar el foco a un panel, a un encabezado tras navegar, o para el roving tabindex.
  • tabindex positivo (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 un tabindex="1" perdido para desordenar toda la página. Si necesitas cambiar el orden, reordena el HTML.

  1. 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.

  1. 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) dale tabindex="-1" primero, o el .focus() no tendrá efecto.

  1. 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 Tab y nunca lo suelta.
  • Un manejador de teclado que hace event.preventDefault() en Tab sin 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.

  1. 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í.

  1. 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.

  1. 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 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.

  1. 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

  • onclick en 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ñade tabindex="0" y maneja Enter y Espacio.
  • tabindex positivo. Rompe el orden de foco de toda la página. Prohibido.
  • outline: none sin alternativa. Deja a los usuarios de teclado sin saber dónde están. Usa :focus-visible.
  • Cancelar Tab sin mover el foco. Crea una trampa (2.1.2). Si interceptas Tab, 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?

<a href="#main" class="skip-link">Saltar al contenido</a>
<main id="main">…</main>

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.

© Copyright 2026. Todos los derechos reservados