Ver contenido
Guía de Implementación de Navegación por Teclado: Construyendo Interfaces Completamente Accesibles
Introducción
La navegación por teclado es un requisito de nivel A en WCAG 2.2, lo que significa que es el nivel mínimo de accesibilidad. Los criterios 2.1.1 (Teclado) y 2.1.2 (Sin Trampa de Teclado) establecen que toda la funcionalidad de una página debe ser operable mediante teclado, y que el usuario nunca debe quedar atrapado en un componente sin poder salir.
Esto afecta a más personas de lo que suele asumirse. Los usuarios de lectores de pantalla navegan exclusivamente con teclado. Las personas con discapacidades motoras que usan dispositivos de entrada alternativos (switches, punteros de cabeza) generan eventos de teclado, no de ratón. Los usuarios avanzados prefieren atajos de teclado por eficiencia. Según WebAIM, los problemas de accesibilidad de teclado se encuentran entre los cinco errores más comunes en auditorías de sitios web.
Esta guía cubre los patrones de implementación fundamentales: gestión de tabindex, manejo de eventos de teclado, el patrón roving tabindex para widgets compuestos, trampas de foco para modales, y skip links.
Fundamentos de gestión de foco
El atributo tabindex
El atributo tabindex controla cómo los elementos participan en la navegación por Tab. Tiene tres valores funcionales con comportamientos muy diferentes:
tabindex="0": El elemento entra en el orden natural de tabulación, determinado por su posición en el DOM. Es el valor correcto para hacer enfocable un elemento personalizado como un<div>conrole="button".tabindex="-1": El elemento puede recibir foco programáticamente (medianteelement.focus()en JavaScript), pero no aparece en el flujo de Tab. Es útil para contenedores de diálogo o destinos de skip links.tabindex="1+"(valores positivos): El elemento salta al frente del orden de tabulación. Parece útil pero causa problemas graves: el flujo de Tab deja de seguir el orden visual de la página, desorientando a los usuarios. El W3C recomienda explícitamente no usar valores positivos.
Los elementos HTML interactivos (<a href>, <button>, <input>, <select>, <textarea>) son enfocables de forma nativa sin necesidad de tabindex. Esta es la razón principal para usar elementos nativos en lugar de construir controles personalizados con <div>: el comportamiento de teclado viene incluido.
Orden de foco y orden visual
El orden de tabulación sigue el orden del DOM, no la disposición visual en pantalla. Si usas CSS flex-direction: column-reverse o order para reorganizar elementos visualmente, el Tab seguirá recorriendo los elementos según aparecen en el HTML. Esta discordancia entre lo que el usuario ve y lo que experimenta con el teclado es una fuente frecuente de confusión.
La solución preferida es que el orden del DOM coincida con el orden visual. Cuando esto no es posible, documenta el comportamiento y verifica que la secuencia de tabulación siga siendo lógica incluso si difiere del layout visual.
Manejadores de eventos de teclado
Cuando creas componentes personalizados que no usan elementos HTML nativos, necesitas implementar manualmente los eventos de teclado que el navegador proporcionaría automáticamente. El patrón básico captura el evento keydown y ejecuta la acción correspondiente según la tecla presionada.
function handleKeyboard(element, actions) {
element.addEventListener('keydown', (e) => {
const handlers = {
'Enter': actions.activate,
' ': actions.activate,
'ArrowUp': actions.previous,
'ArrowDown': actions.next,
'Home': actions.first,
'End': actions.last,
'Escape': actions.cancel
};
const handler = handlers[e.key];
if (handler) {
e.preventDefault();
handler(e);
}
});
}
Dos detalles son importantes aquí. Primero, e.preventDefault() evita que el navegador ejecute el comportamiento por defecto de la tecla (por ejemplo, que la barra espaciadora haga scroll). Segundo, se usa e.key en lugar de e.keyCode, ya que keyCode está deprecado y key proporciona valores legibles como 'Enter' o 'ArrowDown'.
Para un <div> que actúa como botón, la implementación mínima requiere que tanto Enter como Espacio activen la acción, y que el elemento tenga role="button" y tabindex="0":
<div role="button" tabindex="0" onclick="handleClick()">Acción</div>
element.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
element.click();
}
});
Sin embargo, la recomendación siempre es preferir <button> nativo, que incluye todo este comportamiento sin código adicional.
Patrón roving tabindex para widgets compuestos
Los widgets compuestos como barras de herramientas, listas de pestañas y menús necesitan un comportamiento especial: solo un elemento del grupo es accesible mediante Tab, y las flechas del teclado mueven el foco entre los elementos internos. Este patrón se llama “roving tabindex” porque el tabindex="0" se mueve entre elementos según el usuario navega.
El concepto es que el widget completo ocupa una sola parada de Tab en el flujo de la página. Una barra de herramientas con diez botones no debería requerir diez pulsaciones de Tab para atravesarla. En su lugar, Tab lleva al widget, las flechas navegan dentro, y el siguiente Tab sale del widget hacia el siguiente componente de la página.
La implementación asigna tabindex="0" al elemento activo y tabindex="-1" a los demás. Cuando el usuario presiona una flecha, el tabindex se intercambia y el foco se mueve al nuevo elemento:
// Al inicializar: solo el primer elemento tiene tabindex="0"
items.forEach((item, i) => {
item.setAttribute('tabindex', i === 0 ? '0' : '-1');
});
// Al navegar con flechas:
function moveFocus(fromIndex, toIndex) {
items[fromIndex].setAttribute('tabindex', '-1');
items[toIndex].setAttribute('tabindex', '0');
items[toIndex].focus();
}
Las teclas Home y End deben llevar al primer y último elemento respectivamente. Las flechas deben hacer un ciclo: desde el último elemento, ArrowDown vuelve al primero.
Trampa de foco para diálogos modales
El criterio 2.1.2 prohíbe las trampas de teclado, pero los diálogos modales son la excepción legítima: cuando un modal está abierto, el foco debe permanecer dentro del modal hasta que el usuario lo cierre. Esto evita que el usuario interactúe accidentalmente con contenido detrás del modal que no es visible.
La implementación tiene tres fases. Al abrir el modal, se guarda el elemento que tenía el foco antes de la apertura, se identifican todos los elementos enfocables dentro del modal, y se mueve el foco al primero de ellos. Mientras el modal está abierto, cuando Tab llega al último elemento enfocable, el siguiente Tab vuelve al primero (y Shift+Tab desde el primero lleva al último). Al cerrar, el foco vuelve al elemento que lo tenía antes de la apertura.
function activateFocusTrap(modal) {
const focusables = modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusables[0];
const last = focusables[focusables.length - 1];
modal.addEventListener('keydown', (e) => {
if (e.key !== 'Tab') return;
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
});
first.focus();
}
La tecla Escape siempre debe cerrar el modal. Este comportamiento es esperado por todos los usuarios de teclado y está documentado en WAI-ARIA Authoring Practices como requisito del patrón de diálogo.
Skip links: saltar bloques repetitivos
Los skip links son enlaces ocultos que aparecen cuando reciben foco y permiten a los usuarios de teclado saltar directamente al contenido principal sin recorrer toda la navegación. Son un requisito de WCAG 2.4.1 (Saltar Bloques).
La implementación combina HTML, CSS y un poco de JavaScript. El enlace apunta al id del elemento <main>, se oculta fuera de la pantalla hasta que recibe foco, y el destino necesita tabindex="-1" para poder recibir foco programático:
<a href="#main-content" class="skip-link">Saltar al contenido principal</a>
<!-- ... navegación ... -->
<main id="main-content" tabindex="-1">
.skip-link {
position: absolute;
top: -100%;
left: 50%;
transform: translateX(-50%);
padding: 1rem 2rem;
background: #000;
color: #fff;
z-index: 9999;
}
.skip-link:focus { top: 0; }
Si el sitio tiene múltiples regiones importantes (contenido principal, búsqueda, navegación secundaria), se pueden ofrecer varios skip links. Cada uno debe tener un texto descriptivo y claro sobre su destino.
Referencia rápida de teclas por componente
Cada tipo de widget tiene convenciones de teclado documentadas en WAI-ARIA Authoring Practices que los usuarios de tecnologías asistivas esperan encontrar:
| Componente | Teclas requeridas | Comportamiento |
|---|---|---|
| Botón | Enter, Espacio | Activa la acción del botón |
| Enlace | Enter | Navega al destino del enlace |
| Menú desplegable | Flechas, Enter, Escape | Flechas navegan, Enter selecciona, Escape cierra |
| Pestañas | Flechas izq/der, Enter | Flechas cambian pestaña activa |
| Diálogo modal | Tab (ciclo interno), Escape | Tab cicla dentro del modal, Escape cierra |
| Listbox | Flechas, Home, End | Flechas navegan opciones, Home/End van a extremos |
| Acordeón | Enter, Espacio | Expande o colapsa la sección |
Lista de verificación
- [ ] Todos los elementos interactivos son accesibles con teclado
- [ ] El orden de tabulación sigue el orden visual de lectura
- [ ] No se usan valores de tabindex positivos
- [ ] Widgets compuestos implementan roving tabindex
- [ ] Enter y Espacio activan botones y controles
- [ ] Las teclas de flecha navegan dentro de widgets compuestos
- [ ] Escape cierra modales, menús y popups
- [ ] No existen trampas de teclado (excepto modales abiertos)
- [ ] Skip links permiten saltar la navegación repetitiva
- [ ] El foco se restaura al cerrar modales y menús
- [ ] Los indicadores de foco son visibles y tienen contraste suficiente
Preguntas frecuentes
¿Por qué no debo usar tabindex con valores positivos?
Los valores positivos de tabindex (1, 2, 3…) colocan el elemento antes de todos los elementos con tabindex="0" en el orden de tabulación, independientemente de su posición en el DOM. Esto crea un flujo de Tab que no coincide con el orden visual de la página, desorientando a los usuarios. El W3C recomienda explícitamente evitarlos y en su lugar organizar el DOM para que el orden natural sea el correcto.
¿Cuál es la diferencia entre roving tabindex y aria-activedescendant?
Ambos patrones resuelven el mismo problema (navegación dentro de widgets compuestos) de formas diferentes. Roving tabindex mueve el foco real del DOM entre elementos, cambiando los valores de tabindex dinámicamente. aria-activedescendant mantiene el foco en el contenedor y usa un atributo ARIA para indicar cuál elemento hijo está “activo”. Roving tabindex tiene mejor soporte en lectores de pantalla y es el enfoque recomendado por WAI-ARIA Authoring Practices para la mayoría de componentes.
¿Debo implementar todos los atajos de teclado de WAI-ARIA Authoring Practices?
Las teclas documentadas en WAI-ARIA Authoring Practices se dividen en requeridas y opcionales. Las requeridas (Enter, Espacio, Flechas, Escape según el componente) deben implementarse siempre. Las opcionales (como Type-ahead para seleccionar por carácter en listas) mejoran la experiencia pero no son necesarias para el cumplimiento de WCAG.
¿Cómo verifico que no existen trampas de teclado?
La prueba más directa es navegar la página completa usando solo Tab y Shift+Tab. Si en algún momento no puedes avanzar ni retroceder, existe una trampa. Presta especial atención a modales, iframes, reproductores de video, editores de texto enriquecido, y componentes de terceros, que son las fuentes más comunes de trampas de teclado.
Artículos relacionados
- Navegación por Teclado Explicada - Fundamentos y requisitos WCAG
- Guía de Indicadores de Foco - Estilos de foco accesibles
- Hub de Cumplimiento WCAG - Todos los evaluadores de accesibilidad