DEV Community

Silviu Technology
Silviu Technology

Posted on

React: email accesible sin saltos de layout

Un formulario de registro puede funcionar perfectamente y aun asi sentirse lento. El motivo no siempre es la red: a veces el mensaje de validacion aparece debajo del campo, empuja el boton y devuelve el foco a un lugar inesperado. En una interfaz que pide confirmar un email, ese pequeño movimiento cuesta confianza.

En mis formularios de React intento tratar cada respuesta como parte del diseño, no como un parche visual. La meta es sencilla: mostrar que el sistema esta trabajando, explicar que paso y mantener estable la pantalla mientras llega la respuesta.

El problema: un mensaje correcto que mueve toda la interfaz

Imagina un signup con un campo de email y un boton de “Enviar código”. Al pulsarlo, el frontend puede pasar por estos estados:

  1. El campo esta vacio o tiene un formato invalido.
  2. La solicitud esta en curso.
  3. El email fue enviado.
  4. La solicitud fallo y se puede reintentar.

Si cada estado añade o elimina elementos del DOM, el contenido de abajo salta. El salto es molesto con ratón, pero es peor con teclado o lector de pantalla: el usuario puede perder la referencia del control activo. Un spinner por si solo tampoco explica si el código se envio o si hay que corregir el campo.

Antes de implementar el diseño, conviene definir una señal visible para cada estado. Este pequeño inventario tambien ayuda a decidir qué texto debe ser anunciado y qué contenido solo necesita cambiar visualmente.

Para un flujo de pruebas de email, por ejemplo, puede ser razonable usar un generador de burner email en un entorno controlado. El producto debe seguir tratando esa dirección como un dato de prueba, no como una identidad real.

Modelar el estado del email antes de tocar el CSS

Un estado explícito hace que React renderice una intención, no una colección de condiciones sueltas. No hace falta una máquina de estados grande; un tipo pequeño suele bastar:

const [emailState, setEmailState] = useState({
  status: "idle", // idle | sending | sent | error
  message: "",
});

async function requestCode(email) {
  setEmailState({ status: "sending", message: "Enviando código…" });

  try {
    await sendVerification(email);
    setEmailState({ status: "sent", message: "Código enviado. Revisa tu bandeja." });
  } catch {
    setEmailState({ status: "error", message: "No se pudo enviar. Inténtalo de nuevo." });
  }
}
Enter fullscreen mode Exit fullscreen mode

La ventaja practica es que el formulario puede reservar espacio para message incluso cuando esta vacío. Así el estado visual cambia, pero el resto del layout no necesita recolocarse. También evita mezclar un error de red con un error de validación local, que requieren acciones distintas.

Para el contenido de prueba, una dirección disposable mail address puede ayudar a repetir el flujo sin llenar una bandeja personal. En producción, el copy debe indicar las limitaciones y la privacidad del proceso; no conviene esconderlas detrás de un botón bonito.

Un patrón de React y CSS para reservar espacio

La estructura puede ser simple: el label permanece asociado al input, el texto de ayuda tiene un identificador estable y la región de estado vive en un contenedor con altura mínima.

<label htmlFor="email">Email</label>
<input
  id="email"
  name="email"
  type="email"
  aria-describedby="email-help email-status"
  aria-invalid={emailState.status === "error"}
/>
<p id="email-help" className="help-text">
  Te enviaremos un código para continuar.
</p>
<p id="email-status" className="status" aria-live="polite">
  {emailState.message}
</p>
Enter fullscreen mode Exit fullscreen mode
.status {
  min-height: 1.5rem;
  margin-block: 0.35rem 0.75rem;
  color: var(--status-color, #445);
}

.status:empty {
  visibility: hidden;
}

@media (prefers-reduced-motion: no-preference) {
  .status:not(:empty) {
    animation: appear 160ms ease-out;
  }
}
Enter fullscreen mode Exit fullscreen mode

min-height reserva una línea sin inventar un mensaje. visibility conserva el espacio y evita que un lector de pantalla anuncie un párrafo vacío. Si el copy puede ocupar dos líneas en móvil, la reserva debe reflejar el idioma y el tamaño de fuente reales, no una cifra elegida mirando solo el escritorio.

Un detalle comun: ocultar el estado con display: none y luego esperar que aria-live anuncie el cambio. En algunos lectores de pantalla eso no produce una experiencia consistente. Es mejor mantener la región en el árbol y actualizar su texto cuando la transición sea relevante.

Accesibilidad: foco, aria-live y teclado

Cuando el envío termina, no muevas el foco automáticamente si el usuario todavía esta leyendo el formulario. El mensaje en aria-live="polite" puede comunicar el resultado mientras el foco sigue en el boton. Si hay un error de validación que impide continuar, entonces sí tiene sentido llevar el foco al primer campo inválido, pero hazlo una sola vez por intento.

También reviso tres cosas a mano:

  • Con Tab, el orden sigue siendo label, input, botón y acciones secundarias.
  • El contraste del mensaje no depende solo de rojo o verde.
  • El botón tiene un estado deshabilitado entendible mientras sending esta activo.

Para mensajes urgentes puede usarse role="alert", pero no lo pondría en cada cambio de carga. Un anuncio demasiado agresivo se vuelve ruido y hace dificil encontrar la información util.

Pruebas rápidas y métricas de experiencia

No hace falta montar una suite enorme para encontrar los fallos principales. Pruebo una dirección válida, una vacía, una respuesta lenta, un error de servidor y un doble clic. Luego repito cada caso con teclado y con viewport estrecho.

Tambien observo si cambia el document.activeElement, si aparece un salto visible y si el mensaje se anuncia una vez. Una métrica útil no es solo el tiempo de la petición: es cuánto tarda el usuario en saber qué hacer a continuación. El frontend puede ser rápido en milisegundos y aun así parecer confuso.

Las consultas humanas tampoco son siempre limpias. Si una página documenta un flujo de email, puede encontrar búsquedas como “tepm mail com” o “tamp mail com”. Esos terminos deben tratarse como señales de búsqueda, no como enlaces ni como nombres que se muestran en un producto serio.

Q&A: decisiones comunes

¿Debo reservar espacio para todos los mensajes?

Reserva el espacio de los mensajes que aparecen en el flujo normal: ayuda, validación y estado de envío. Para un error excepcional muy largo, deja que el contenido crezca sin romper el foco ni tapar controles.

¿Un skeleton es mejor que un spinner?

No siempre. Para una respuesta breve, un texto de estado es más claro. El skeleton funciona cuando ya conoces la forma del contenido que llegará. En un formulario, anunciar “Enviando código” suele ser más útil que pintar bloques grises.

¿Qué reviso antes de publicar?

Comprueba el foco, el contraste, el texto de aria-live, la navegación sin ratón y el comportamiento con zoom. Después mira la versión móvil: una frase que cabe en escritorio puede crear dos líneas y un salto en una pantalla pequeña.

El resultado no es un formulario más decorado. Es una interfaz que comunica intención, conserva contexto y hace que la accesibilidad forme parte del rendimiento percibido. Ese es el tipo de pulido que se nota incluso cuando nadie sabe nombrarlo.

Si el onboarding necesita más señales de producto, puede ser útil medir señales útiles en un onboarding. Y para el caso concreto de esperar un email sin mover controles, conviene mantener el formulario estable mientras llega el email.

Top comments (0)