Un estado de carga parece una pieza pequeña de la interfaz. Sin embargo, decide si una persona entiende lo que ocurre o pulsa el botón varias veces. En un formulario de registro, por ejemplo, la espera puede incluir una petición, una validación y la llegada de un email. Si cada paso cambia el layout, el producto se siente lento aunque la red no sea el principal problema.
Mi regla práctica es esta: una carga debe conservar el espacio, explicar el siguiente paso y tener una salida clara si algo falla. No hace falta una animación espectacular. Hace falta que el diseño, React y la accesibilidad cuenten la misma historia.
El problema no es esperar, es no entender la espera
Un patrón bastante común muestra un spinner después de pulsar “Continuar”. El botón desaparece, aparece un texto debajo y, unos milisegundos después, todo vuelve a su sitio. Ese salto visual aumenta la sensación de inestabilidad y puede hacer que alguien pulse otra vez.
También hay otro fallo: usar el mismo estado para “estamos comprobando”, “se envió el email” y “no pudimos completar la acción”. Son situaciones diferentes. El usuario necesita saber si debe esperar, revisar su bandeja o corregir un dato.
En flujos de QA aparecen búsquedas como tempmailso, temp mail so y hasta el typo tempail. No son estados de la interfaz, claro, pero sirven para recordar que la parte de email tiene varios pasos. El frontend no debería presentar todos como un simple “cargando”.
Un estado de carga que conserva el layout
Primero defino un contrato pequeño para el componente:
-
idle: el formulario acepta interacción. -
loading: la petición está en curso y la acción se puede bloquear. -
success: la operación terminó y se explica qué hacer ahora. -
error: hay un mensaje útil y una acción para intentarlo de nuevo.
El espacio reservado es importante. En vez de insertar un aviso que empuja el formulario, dejo un contenedor con una altura mínima. Así, el botón y los campos no bailan cuando cambia el mensaje. Es una decisión de CSS, pero mejora el rendimiento percibido sin fingir que la operación terminó antes.
Un componente sencillo puede ser suficiente:
type Status = "idle" | "loading" | "success" | "error";
type StatusMessageProps = {
status: Status;
message?: string;
};
export function StatusMessage({ status, message }: StatusMessageProps) {
if (status === "idle") return <div className="status-space" aria-hidden="true" />;
return (
<p
className={`status status--${status}`}
role={status === "error" ? "alert" : "status"}
aria-live={status === "error" ? "assertive" : "polite"}
>
{message}
</p>
);
}
El estado idle conserva el hueco, mientras que los demás estados ofrecen información. En una aplicación real, evitaría que message sea una cadena genérica como “Error”. El texto debería decir qué pasó y cuál es la siguiente acción, por ejemplo: “No recibimos respuesta. Intenta de nuevo en unos segundos”.
React y CSS para feedback accesible
El atributo aria-busy puede indicar que una región está siendo actualizada. No lo pondría en toda la página: prefiero aplicarlo al bloque que realmente cambia.
<section aria-busy={status === "loading"} aria-labelledby="signup-title">
<h2 id="signup-title">Crear cuenta</h2>
<EmailForm disabled={status === "loading"} />
<StatusMessage status={status} message={message} />
</section>
Para el estilo, una reserva mínima y una animación discreta suelen bastar:
.status-space {
min-block-size: 2.75rem;
}
.status {
min-block-size: 2.75rem;
margin-block: 0.75rem 0;
padding: 0.65rem 0.8rem;
border-radius: 0.4rem;
}
.status--loading {
color: #394150;
background: #f1f4f8;
}
@media (prefers-reduced-motion: reduce) {
.status {
animation: none;
}
}
Hay un detalle que se olvida facil: el contraste y el foco. Un color amarillo claro puede verse “amable” en el diseño, pero no comunica nada si el texto se pierde. También conviene revisar el flujo con teclado y con un lector de pantalla; el spinner por si solo no explica el progreso.
Cuando el proceso depende de varios eventos, una evidencia separada ayuda al equipo. Por ejemplo, puedes guardar un recibo de email para pruebas automatizadas y conectar ese resultado con el estado visible. Si el backend necesita aislamiento entre entornos, revisa también el patrón de emails aislados por rama de desarrollo.
Medir la experiencia sin inventar precisión
No mediría solo el tiempo hasta que desaparece el spinner. Una vista más util es registrar:
- cuánto tarda en aparecer el primer feedback;
- cuánto tiempo permanece cada estado;
- cuántas veces se repite la acción;
- cuántos errores terminan en reintento o abandono.
Estas señales ayudan a separar un problema de backend de uno de comunicación. Si la respuesta llega rapido pero muchas personas repiten el clic, quizá el botón no muestra suficiente confirmación. Si el estado de error aparece sin una acción clara, el tiempo de red no cuenta toda la historia.
Checklist antes de publicar
- ¿El layout conserva el espacio durante la carga?
- ¿El botón tiene foco y queda deshabilitado solo cuando corresponde?
- ¿El mensaje diferencia espera, éxito y error?
- ¿Un lector de pantalla recibe el cambio sin demasiados anuncios?
- ¿La interfaz funciona con teclado y con
prefers-reduced-motion? - ¿El test cubre una respuesta lenta y un reintento?
Q&A rápido
¿Siempre necesito un skeleton?
No. Un skeleton funciona cuando conoces la forma del contenido que llegará. Para una acción breve, un mensaje estable y un botón con estado claro suele ser más honesto.
¿Debo mostrar porcentaje de progreso?
Solo si puedes calcularlo de forma fiable. Un porcentaje inventado crea más dudas. Para una petición de email, “Comprobando el envío” puede ser mejor que avanzar de 10% a 90% sin evidencia.
¿Qué aporta CSS al rendimiento?
No reduce por sí solo el tiempo del servidor, pero puede evitar saltos, repintados innecesarios y confusión. Esa combinación hace que la interfaz parezca más rapida y, sobre todo, sea más entendible.
Top comments (0)