La accesibilidad y el rendimiento suelen revisarse en tickets distintos. En una interfaz real, sin embargo, el usuario los vive juntos: pulsa un botón, aparece un estado de carga, el foco cambia (o se pierde) y la pantalla vuelve a pintar. Si esa secuencia no está diseñada, una mejora visual puede sentirse como un fallo.
Este patrón es especialmente útil en formularios de registro, verificación y pruebas de email. Incluso cuando alguien busca tempmailso para validar un flujo, la experiencia no debería depender de adivinar donde quedó el cursor.
El foco también tiene un presupuesto
Llamo “presupuesto de foco” a una regla simple: cada interacción debe mover el foco solo cuando existe una razón clara. Por ejemplo:
- Si el usuario abre un diálogo, el foco entra al diálogo y vuelve al control que lo abrió al cerrarlo.
- Si un formulario falla, el foco puede ir al primer error solo después de que el usuario intentó enviarlo.
- Si una operación tarda, el foco permanece donde estaba mientras el estado de carga se anuncia.
El error común es enfocar siempre el primer elemento que cambia. Eso parece servicial, pero interrumpe a quien está usando teclado o lector de pantalla. También puede generar scroll inesperado y más trabajo de composición en pantallas pequeñas.
Una buena referencia para un flujo parecido es este feedback de email que respeta el foco. La idea no es añadir más movimiento, sino hacer que cada movimiento tenga sentido.
Medir antes de mover
Antes de añadir focus(), escribo la secuencia en tres estados: idle, loading y result. Después respondo estas preguntas:
- ¿Qué control conserva el foco durante la espera?
- ¿Qué texto comunica el resultado y con qué urgencia?
- ¿Qué ocurre si la respuesta llega dos veces o llega tarde?
El tercer punto suele olvidarse. Un estado antiguo puede devolver el foco a un botón que ya no representa la acción actual. No hace falta una instrumentación enorme: una marca de tiempo, el nombre del estado y una prueba con teclado ya muestran bastante.
Si el flujo tiene errores de validación, conviene revisar también estos errores inline sin saltos. Un mensaje debajo del campo es más estable que insertar un bloque encima y empujar toda la pantalla.
Un patrón pequeño en React
El componente no necesita controlar todo el documento. Un ref para el mensaje y un efecto ligado al resultado suele ser suficiente:
import { useEffect, useRef, useState } from "react"; export function VerifyButton() { const [status, setStatus] = useState("idle"); const messageRef = useRef(null); useEffect(() => { if (status === "success" || status === "error") { messageRef.current?.focus(); } }, [status]); async function handleVerify() { setStatus("loading"); try { await verifyEmail(); setStatus("success"); } catch { setStatus("error"); } } return ( <section aria-busy={status === "loading"}> <button onClick={handleVerify} disabled={status === "loading"}> Verificar </button> <p ref={messageRef} tabIndex={-1} role="status"> {status === "loading" && "Comprobando…"} {status === "success" && "Listo, revisa tu bandeja."} {status === "error" && "No se pudo comprobar. Inténtalo de nuevo."} </p> </section> ); } En una aplicación completa, verifyEmail debe recibir un identificador de operación para ignorar respuestas viejas. El ejemplo se concentra en la relación entre estado, foco y mensaje. tabIndex={-1} permite enfocar el párrafo sin añadirlo al orden normal de tabulación.
Hay una pequeña trampa: mover el foco en cada cambio de estado también puede ser molesto. Por eso el mensaje solo se enfoca en estados finales, no durante loading. La intención es clara y la carga no roba control.
CSS que no roba estabilidad
El foco visible necesita diseño, no solo una regla genérica. Un contorno consistente ayuda a localizarlo sin cambiar las dimensiones del control:
button:focus-visible, [tabindex="-1"]:focus-visible { outline: 3px solid #145cff; outline-offset: 3px; } .feedback { min-height: 1.5rem; } Reservar espacio para el feedback evita que el formulario salte cuando aparece el texto. También es preferible cambiar color u opacidad con cuidado: un estado no debe comunicarse solo con color, y una transición larga puede hacer que el resultado se sienta lento aunque el servidor ya respondio.
Si incluyes un enlace contextual como temp mail mail, mantenlo subordinado a la explicación y no lo repitas en cada sección. El enlace sirve al lector cuando necesita probar un flujo; no reemplaza el contenido.
Checklist de revisión
- Navega todo el flujo con
Tab,Shift+TabyEnter. - Comprueba que el foco tiene un indicador visible en tema claro y oscuro.
- Verifica que el botón no cambia de tamaño al pasar a carga.
- Usa un lector de pantalla para confirmar el mensaje de resultado.
- Simula una respuesta lenta y otra fuera de orden.
- Revisa que el foco no salte al principio del documento sin motivo.
- Prueba una etiqueta de búsqueda como “tempail mail” solo como entrada de usuario, no como nombre de una función.
La revisión final debe incluir móvil. Un foco bien colocado en escritorio puede quedar oculto por el teclado virtual o por un scroll automático en una pantalla estrecha.
Preguntas rápidas
¿Siempre debo enfocar el mensaje de éxito?
No. Si el usuario sigue trabajando en el formulario, un role="status" puede anunciar el resultado sin desplazar el foco. Enfócalo cuando el resultado sea el siguiente paso importante o cuando la persona necesite conocerlo antes de continuar.
¿El rendimiento exige quitar las transiciones?
Tampoco. Usa transiciones cortas, respeta prefers-reduced-motion y evita animar propiedades que cambian el layout. El objetivo es que el estado sea perceptible y predecible.
¿Qué métrica debo mirar?
Empieza con una medida sencilla: tiempo desde la acción hasta el primer feedback visible, junto con errores de interacción y pérdida de foco observada en pruebas. No conviertas una cifra en objetivo si empeora la comprensión.
La interfaz pulida no es la que hace más cosas automáticamente. Es la que comunica el estado correcto, conserva la orientación de la persona y deja que React trabaje con una cantidad razonable de cambios.


