Lección 3 de 10 · 14 min de lectura · Revisado el 26/10/2026
Cómo funciona el foco: enfocable, tabulable, eventos y contenido oculto
En la lección anterior vimos que todo debe ser alcanzable, activable y completable con teclado. Para conseguirlo, y para diagnosticar por qué algo falla, hace falta entender qué es exactamente el foco y cómo lo gestiona el navegador. Esta lección explica el modelo de foco del navegador, cómo observarlo desde JavaScript y CSS, el criterio 3.2.1 Al recibir el foco y el problema del «foco fantasma» en el contenido oculto.
Enfocable no es lo mismo que tabulable
El foco es el estado del único elemento del documento que recibe en ese momento las pulsaciones de teclado. Como máximo hay un elemento con foco por documento; si ninguno lo tiene, document.activeElement devuelve el <body>. La especificación HTML distingue dos niveles:
- Enfocable: el elemento puede recibir el foco de alguna manera, ya sea con Tab, con un clic o por programa con
elemento.focus(). - Tabulable (en la especificación, «enfocable secuencialmente»): además forma parte de la secuencia que recorren Tab y Mayús+Tab.
| Elemento | ¿Enfocable? | ¿Tabulable? |
|---|---|---|
a con href | Sí | Sí |
a sin href | No | No |
button, input, select, textarea habilitados | Sí | Sí |
Los mismos con el atributo disabled | No | No |
summary de un details | Sí | Sí |
Elemento con contenteditable | Sí | Sí |
iframe | Sí: el foco entra en su documento | Sí |
div, span, p, encabezados | No | No |
Cualquier elemento con tabindex="0" | Sí | Sí |
Cualquier elemento con tabindex="-1" | Sí | No |
disabled frente a aria-disabled
El atributo disabled saca el control de la secuencia de tabulación. Tiene un coste: quien tabula por un formulario no sabe que existe el botón «Enviar solicitud» hasta que se habilita, y no puede llegar a él para oír por qué está desactivado. La alternativa es aria-disabled="true", que comunica el estado a las tecnologías de apoyo pero mantiene el elemento enfocable. A cambio, el bloqueo de la acción lo tiene que hacer tu código:
<button type="submit" aria-disabled="true" aria-describedby="aviso-envio">Enviar solicitud</button>
<p id="aviso-envio">Completa los campos obligatorios para poder enviar.</p>
boton.addEventListener('click', (e) => {
if (boton.getAttribute('aria-disabled') === 'true') {
e.preventDefault(); // no se envía, pero el botón sigue localizable
return;
}
enviar();
});
Contenedores con desplazamiento
Una caja con overflow: auto (una tabla ancha, unas condiciones legales largas) solo se puede desplazar con las flechas si algo dentro de ella tiene el foco. Si su contenido no incluye ningún elemento enfocable, la persona que usa teclado no puede leer lo que queda oculto, y eso incumple 2.1.1. Firefox y las versiones recientes de Chrome y Edge hacen enfocables estos contenedores automáticamente, pero no todos los navegadores lo hacen. Para no depender de ello, hazlo explícito y dale un nombre para que el lector de pantalla anuncie qué es:
<div class="tabla-ancha" tabindex="0" role="region" aria-label="Tarifas 2026, tabla con desplazamiento horizontal">
<table>…</table>
</div>
Observar el foco desde JavaScript y CSS
Gestionar el foco exige saber leerlo. Estas son las piezas que vas a usar continuamente:
document.activeElementdevuelve el elemento que tiene el foco.- Los eventos
focusyblurse disparan en el elemento que gana o pierde el foco y no burbujean. Sus equivalentesfocusinyfocusoutsí burbujean, así que puedes escucharlos en un contenedor; su propiedadrelatedTargetindica a dónde va (o de dónde viene) el foco. elemento.focus()mueve el foco y desplaza la vista hasta el elemento. Confocus({ preventScroll: true })no se desplaza; úsalo con cuidado, porque el elemento enfocado tiene que quedar a la vista.- En CSS,
:focusse aplica a cualquier elemento con foco;:focus-visible, solo cuando el navegador considera que conviene mostrar el indicador (al navegar con teclado y, en los campos de texto, casi siempre);:focus-within, al elemento que tiene el foco o contiene al que lo tiene, útil para resaltar una tarjeta o mantener abierto un submenú.
Ejemplo
Cerrar un desplegable cuando el foco sale de él, sin cerrarlo mientras la persona se mueve por sus opciones:
desplegable.addEventListener('focusout', (e) => {
// relatedTarget es el elemento que recibe el foco (o null)
if (!desplegable.contains(e.relatedTarget)) {
cerrarDesplegable();
}
});3.2.1 Al recibir el foco: enfocar no es decidir
Este criterio de nivel A exige que, cuando un componente recibe el foco, no se inicie ningún cambio de contexto: abrir una ventana nueva, mover el foco a otro componente, enviar un formulario o cambiar de forma sustancial el contenido de la página. Quien navega con teclado o con lector de pantalla recorre los elementos para explorarlos; si el mero hecho de pasar por uno dispara una acción, la persona pierde el control.
Fallos típicos: avanzar al paso siguiente de un formulario cuando el foco llega al último campo, abrir una ventana emergente al enfocar un enlace, o lanzar una búsqueda al entrar en el buscador. Lo que sí está permitido es mostrar información adicional, como un tooltip (una etiqueta emergente) o un resaltado, porque no cambia el contexto; ese contenido debe cumplir 1.4.13 Contenido con hover o foco, que verás en el curso de componentes interactivos.
Error frecuente
<a href="/tramites" onfocus="this.blur()">Trámites</a>Se usaba para que el indicador de foco no apareciera nunca. Al quitar el foco en cuanto llega, el enlace se vuelve inalcanzable y el foco salta a un lugar imprevisible. El W3C lo documenta como el fallo F55, que incumple a la vez 2.1.1 Teclado, 2.4.7 Foco visible y 3.2.1 Al recibir el foco. Si molesta el indicador al hacer clic, la solución es :focus-visible, no quitar el foco.
Contenido oculto y el «foco fantasma»
Si un elemento no se ve pero sigue siendo tabulable, la persona pulsa Tab y el indicador desaparece: el foco está en un enlace del menú móvil plegado fuera de la pantalla, en la cuarta diapositiva de un carrusel o dentro de un acordeón cerrado con height: 0. Lo llamamos foco fantasma. Incumple 2.4.7 Foco visible y, a menudo, 2.4.3 Orden del foco, porque el recorrido pasa por elementos que no están. El curso de CSS trata la ocultación en general; aquí lo que importa es qué hace cada técnica con el foco.
Infografía · Conceptos clave
Ocultar contenido: qué pasa con el foco
Seis formas habituales de esconder algo y si sus elementos siguen en la secuencia de Tab
- display:none y hiddenNi se ve, ni recibe el foco, ni llega al lector de pantalla
- visibility:hiddenNo es enfocable y se puede animar: útil en menús que se deslizan
- inertVisible, pero ni enfocable ni perceptible: el fondo de un modal
- aria-hidden="true"Sale del lector pero sigue enfocable: nunca con enlaces dentro
- Fuera de pantallaCon transform o posición negativa sigue en la secuencia de Tab
- opacity:0 o height:0Invisible pero tabulable: es el origen del foco fantasma
Leer la infografía como texto
Seis tarjetas sobre técnicas de ocultación y su efecto en el foco. Primera: display:none y el atributo hidden; el contenido no se ve, no recibe el foco y no llega al lector de pantalla. Segunda: visibility:hidden; tampoco es enfocable y se puede animar, útil para menús que se deslizan. Tercera: inert; el contenido sigue visible, pero no es enfocable ni perceptible, adecuado para el fondo de un modal. Cuarta: aria-hidden="true"; lo quita del lector pero sigue siendo enfocable, así que nunca debe envolver enlaces ni botones. Quinta: contenido fuera de pantalla con transform o posición negativa; sigue en la secuencia de Tab. Sexta: opacity:0 o height:0; es invisible pero tabulable y provoca el foco fantasma. Las tres primeras sacan el contenido del foco; las tres últimas, no.
De las técnicas habituales, cuatro sacan el contenido de la secuencia de tabulación: display: none, el atributo hidden, visibility: hidden (que además se puede animar) e inert (que deja el contenido visible, pero no enfocable ni perceptible). Las demás (posicionarlo fuera de pantalla, transform, opacity: 0, height: 0, clip-path) lo esconden a la vista pero lo dejan tabulable. Y aria-hidden="true" hace lo contrario: lo quita del lector de pantalla pero no del foco, así que nunca debe envolver enlaces ni botones.
Ejemplo
Incorrecto: un menú lateral que se esconde solo desplazándolo. Sus 15 enlaces siguen en la secuencia de Tab aunque el menú esté cerrado.
.menu-lateral { transform: translateX(-100%); transition: transform .3s; }
.menu-lateral.abierto { transform: none; }Correcto: se añade visibility, que también se puede animar, y los enlaces salen de la secuencia mientras el menú está cerrado.
.menu-lateral {
transform: translateX(-100%);
visibility: hidden;
transition: transform .3s, visibility .3s;
}
.menu-lateral.abierto { transform: none; visibility: visible; }Fronteras del foco: iframes y shadow DOM
Al tabular, el foco entra en el documento de un iframe, lo recorre y sale por el final. Si el contenido incrustado (un reproductor, un mapa, un chat de terceros) atrapa el foco o no muestra indicador, el problema es de tu página aunque no hayas escrito ese código, porque la conformidad se evalúa sobre la página completa. Prueba con teclado cualquier componente de terceros antes de incrustarlo.
Los elementos enfocables de un componente web con shadow DOM (un subárbol encapsulado dentro de un elemento anfitrión) se recorren en la posición del anfitrión. Dos detalles que confunden al depurar: document.activeElement devuelve el anfitrión, no el elemento interno (para eso está anfitrion.shadowRoot.activeElement), y si creas la raíz con attachShadow({ mode: 'open', delegatesFocus: true }), al enfocar el anfitrión el foco pasa automáticamente a su primer elemento enfocable.
Pruébalo tú
Abre en Chrome o Edge una web que uses a menudo. Abre las herramientas para desarrolladores (F12), ve a la pestaña Consola y pulsa el icono del ojo («Crear expresión en directo»). Escribe document.activeElement. Ahora tabula por la página: la expresión mostrará en cada momento qué elemento tiene el foco. Busca una parada en la que no veas nada en pantalla: habrás encontrado un foco fantasma o un indicador eliminado.
Punto clave
Antes de corregir un problema de teclado, averigua dónde está realmente el foco. Lo que se oculta debe salir de la secuencia de tabulación (hidden, display: none, visibility: hidden o inert), nunca solo de la vista o solo del lector, y recibir el foco nunca debe disparar un cambio de contexto.
Resumen
- Enfocable significa que puede recibir el foco; tabulable, que además está en la secuencia de Tab.
disabledsaca el control de la secuencia;aria-disabledlo mantiene. - Haz enfocables (con nombre) los contenedores con desplazamiento que no tienen nada enfocable dentro.
- Usa
focusinyfocusoutconrelatedTargetpara saber cuándo el foco entra o sale de un componente. - 3.2.1: recibir el foco no puede abrir ventanas, enviar formularios ni mover el foco a otro sitio.
- El contenido oculto debe salir también de la secuencia de tabulación;
aria-hiddensobre elementos enfocables es un error, y el contenido deiframey shadow DOM también forma parte de tu página.
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.