DEV Community

Silviu Technology
Silviu Technology

Posted on

React: formularios de email sin perder el foco

Un formulario que pide un correo temporal parece sencillo: un campo, un botón y un mensaje cuando termina. En la práctica, el estado de la interfaz cambia varias veces. El botón pasa a cargando, llega una respuesta lenta, aparece un error o se muestra el email recibido. Si cada cambio mueve el foco o cambia el tamaño de la pantalla, la experiencia se vuelve confusa, especialmente para quien usa teclado o un lector de pantalla.

En React prefiero tratar este flujo como una pequeña máquina de estados visible. La interfaz debe decir qué está pasando, conservar el punto de interacción y evitar trabajo visual innecesario. Esto funciona igual si usamos un generador de cuentas de correo temporal para una prueba o una bandeja de correo de una aplicación real.

El problema: el formulario cambia demasiado

Un patrón común es renderizar un componente distinto para cada paso:

return status === "loading" ? <Spinner /> : <Form />;
Enter fullscreen mode Exit fullscreen mode

Es corto, pero puede desmontar el campo justo después de pulsar el botón. El foco se pierde y el usuario tiene que buscar de nuevo dónde está. También puede provocar un salto de layout cuando el spinner ocupa un espacio diferente al formulario.

Una estructura más estable conserva el formulario y cambia sólo sus partes internas:

<form onSubmit={handleSubmit} aria-busy={status === "loading"}>
  <label htmlFor="email">Correo para la prueba</label>
  <input
    id="email"
    name="email"
    type="email"
    value={email}
    onChange={(event) => setEmail(event.target.value)}
    aria-invalid={Boolean(error)}
    aria-describedby={error ? "email-error" : "email-help"}
  />
  <p id="email-help">Usaremos esta dirección sólo para el flujo de prueba.</p>
  {error && <p id="email-error" role="alert">{error}</p>}
  <button type="submit" disabled={status === "loading"}>
    {status === "loading" ? "Comprobando…" : "Comprobar correo"}
  </button>
</form>
Enter fullscreen mode Exit fullscreen mode

El campo sigue en el mismo lugar. Esa continuidad es una mejora de UX pequeña, pero se nota muchisimo cuando se navega sin ratón.

Diseñar estados que se puedan percibir

Antes de escribir CSS, enumero los estados que el usuario puede ver:

  1. idle: el formulario está listo.
  2. loading: la petición está en curso y el botón no debe duplicarla.
  3. success: se confirma que el correo fue recibido o que el paso terminó.
  4. error: la persona puede corregir el dato o volver a intentar.

Cada estado necesita una señal visual y, cuando corresponde, una señal semántica. aria-busy comunica que una región está ocupada, pero no sustituye un mensaje. Para respuestas que llegan de forma asíncrona, un texto con role="status" o aria-live="polite" suele ser más útil:

<p className="form-status" role="status" aria-live="polite">
  {status === "loading" && "Esperando el mensaje de verificación…"}
  {status === "success" && "Listo: revisa el correo de prueba."}
</p>
Enter fullscreen mode Exit fullscreen mode

El mensaje no deberia anunciar cada pulsación. Sólo debe cambiar cuando existe una respuesta relevante. Para un error que exige corrección inmediata, role="alert" es apropiado, pero conviene no abusar de él: demasiadas alertas hacen que la navegación sea agotadora.

Mantener el foco en React

Cuando el envío falla, el foco debería volver al campo que tiene el problema. Un ref permite hacerlo después de actualizar el estado:

const emailRef = useRef(null);

function showEmailError(message) {
  setError(message);
  requestAnimationFrame(() => emailRef.current?.focus());
}
Enter fullscreen mode Exit fullscreen mode

No movería el foco al éxito automáticamente. El usuario puede querer leer la respuesta, revisar otro control o copiar la dirección. En su lugar, haría que el estado de éxito sea claramente anunciable y mantendría el botón en el mismo sitio.

También es importante no desactivar el botón durante un error permanente. Deshabilitarlo mientras llega la petición evita dobles envíos; después debe volver a estar disponible. Esta diferencia parece menor y suele romperse en una validacion rapida del componente.

Si el equipo está diseñando el backend al mismo tiempo, una referencia como probar un signup sin un inbox real ayuda a separar la prueba del flujo de registro de la apariencia del formulario. Son capas diferentes, pero deben compartir estados comprensibles.

CSS y rendimiento sin efectos bruscos

Reserva un espacio mínimo para el mensaje de estado aunque esté vacío. Así, la aparición del texto no empuja el botón hacia abajo:

.form-status {
  min-height: 1.5rem;
  margin-block: 0.5rem;
}

.form-shell {
  contain: layout;
}

@media (prefers-reduced-motion: reduce) {
  .form-shell * {
    transition: none;
    animation: none;
  }
}
Enter fullscreen mode Exit fullscreen mode

contain: layout no es una solución universal, pero puede limitar el alcance de un cambio de layout en un componente aislado. Primero mediría el efecto y sólo lo dejaría donde el árbol de UI lo necesita. El CSS tambien debe respetar el contraste y no usar color como única señal de error.

Para una respuesta lenta, un indicador discreto es mejor que una animación pesada. Evito montar librerías completas para un spinner de una línea. La interfaz se siente más rapida cuando conserva dimensiones, reduce repaints innecesarios y entrega una explicación breve.

Probar el flujo con un correo temporal

Una prueba útil cubre teclado, lector de pantalla y red lenta, no sólo el caso feliz. Simula al menos estas secuencias:

  • Enviar un valor vacío: el foco queda en el campo y el error se puede leer.
  • Enviar un email válido: el botón muestra progreso y no se duplica la petición.
  • Recibir una respuesta lenta: el usuario ve que la aplicación sigue trabajando.
  • Recibir un error del servidor: el campo sigue disponible y el mensaje explica el siguiente paso.
  • Completar el flujo: el éxito se anuncia sin robar el foco.

Si necesitas una dirección para inspeccionar un flujo externo, un correo temporal para Facebook puede ser una fixture conveniente para un entorno de prueba, siempre que no se use con datos reales ni para saltarse controles de una cuenta. Para revisar la operación completa, también resulta útil conservar alertas de email con evidencia, porque un fallo de entrega no siempre es un fallo del frontend.

En algunos repositorios aparecen nombres históricos como temp org mail. No los convertiría en una etiqueta visible ni en un nombre de usuario: una fixture debe explicar su propósito sin confundir a quien mantiene la interfaz.

Checklist de accesibilidad

Antes de publicar el componente, compruebo:

  • El label está conectado al input.
  • El estado de carga se anuncia y bloquea sólo el doble envío.
  • El error tiene texto, relación con el campo y un foco razonable.
  • El éxito no roba el foco sin una razón clara.
  • El layout conserva espacio para los mensajes.
  • La interfaz funciona con teclado y zoom.
  • prefers-reduced-motion reduce las transiciones.
  • Una prueba de red lenta no deja al usuario sin contexto.

El detalle más importante no es la animación del botón. Es que cada cambio tenga una explicación que pueda percibirse, que el foco permanezca donde se espera y que el formulario siga siendo estable. En React, accesibilidad, CSS y rendimiento se encuentran justo en esos pequeños estados del flujo.

Top comments (0)