Cómo agregar una animación de carga a una app o un sitio web
Por el equipo de Charmotion · Actualizado el

Para agregar una animación de carga, decide primero cuánto dura la espera: no muestres nada si es de menos de un segundo aproximadamente, usa un spinner o un personaje en loop para unos 2 a 10 segundos y una barra con porcentaje completado si es más larga. Luego muéstrala solo mientras haya trabajo pendiente, anúnciala con role="status" y respeta los ajustes de movimiento reducido.
Puntos clave
- Ajusta el indicador a la espera: ninguno por debajo de 1 segundo aproximadamente, un loop para 2 a 10 segundos y una barra de progreso para 10 segundos o más.
- Retrasa un poco el loader para que las respuestas rápidas nunca hagan parpadear un spinner.
- Anima solo
transformyopacitypara que el loader se mantenga fluido mientras el hilo principal está ocupado. - Acompaña el elemento visual con texto en
role="status"yaria-busypara que quienes usan lector de pantalla sepan que algo está pasando. - Un personaje de marca funciona como indicador en loop; no reemplaza una barra de progreso en tareas largas.
- Para un loader con logo, mantén el logo quieto dentro de un anillo giratorio, o dale un pulso suave y continuo; detén el movimiento extra con movimiento reducido.
¿Ya tienes un personaje o logo? Mira tu personaje en pantallas reales de una app: pantallas de splash, onboarding, carga y estado vacío. Es gratis, funciona en tu navegador y no sube nada.
Spinner, skeleton, barra de progreso o loader de marca: ¿cuál elegir?
Elige el indicador según la espera prevista y luego según el contexto. Las pautas de Nielsen Norman Group definen los rangos de tiempo: ningún indicador para acciones de menos de un segundo aproximadamente, un indicador en loop, como un spinner, para esperas de unos 2 a 10 segundos, y un indicador de porcentaje completado para todo lo que tarde 10 segundos o más (NN/g). Esos rangos se basan en los límites de tiempo de respuesta de Nielsen: 0.1 segundos se percibe como instantáneo, 1 segundo mantiene el hilo de pensamiento del usuario y 10 segundos es más o menos el límite de la atención.
| Situación | Uso |
|---|---|
| Menos de ~1 segundo | Sin loader. Da respuesta inmediata en el control (estado presionado, botón desactivado). |
| Página o lista cuyo diseño ya conoces | Un skeleton: marcadores grises con la forma del contenido, que se reemplazan en su lugar. |
| 2 a 10 segundos, progreso desconocido | Un spinner en loop o un personaje en loop. |
| 10 segundos o más, o trabajo medible (cargas, exportaciones) | Una barra de progreso con el porcentaje completado, opcionalmente con un personaje al lado. |
Los skeletons y los spinners no compiten entre sí. Un skeleton describe dónde aparecerá el contenido; un spinner o un personaje indica que se está trabajando. Muchas pantallas usan un skeleton para la primera carga de la página y un spinner pequeño para acciones posteriores como "cargar más". Para ver los detalles de tiempo detrás de estos rangos, consulta duraciones de animación y UX de carga.
¿Cómo crear un loader solo con CSS?
Un spinner en CSS necesita un elemento, una regla de keyframes y nada de JavaScript. Gíralo con transform: web.dev recomienda limitar las animaciones a transform y opacity porque se quedan en la etapa de composición, así que el spinner sigue girando aunque se estén ejecutando scripts (web.dev).
<div class="loader" role="status">
<span class="spinner" aria-hidden="true"></span>
<span class="visually-hidden">Loading results…</span>
</div>
<style>
.spinner {
display: inline-block;
width: 32px;
height: 32px;
border: 3px solid currentColor;
border-right-color: transparent;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
.spinner { animation-duration: 2.4s; }
}
.visually-hidden {
position: absolute; width: 1px; height: 1px;
overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
}
</style>
Dale al spinner un ancho y un alto fijos para que nunca provoque un layout shift (salto de diseño) al aparecer. Usa currentColor para que herede el color del texto tanto en el tema claro como en el oscuro.
Un skeleton es igual de sencillo: bloques con tamaño fijo y un color de fondo, opcionalmente con un brillo creado con un degradado animado que se mueve con transform. Mantén el brillo sutil y quítalo con movimiento reducido.
¿Cómo hacer una animación de carga con tu logo?
Una animación de carga con logo, o un spinner de carga con tu logo, funciona mejor cuando el logo sigue siendo legible. Tres patrones cubren la mayoría de las marcas:
- Logo dentro de un anillo que gira. El logo se queda quieto y un anillo gira a su alrededor. Es la opción más legible y funciona con cualquier forma de logo.
- Un pulso suave. El logo se reduce un pequeño porcentaje y vuelve a su tamaño con un leve cambio de opacidad. Ideal para símbolos compactos e íconos de app.
- El propio logo gira. Solo para logos redondos y simétricos; un logo de texto o un ícono asimétrico se vuelve difícil de leer y puede marear.
<div class="logo-loader" role="status">
<img src="/logo-mark.svg" alt="" width="40" height="40">
<span class="visually-hidden">Loading…</span>
</div>
<style>
.logo-loader {
position: relative; display: grid; place-items: center;
width: 64px; height: 64px;
}
.logo-loader::after {
content: ""; position: absolute; inset: 0;
border: 3px solid currentColor; border-right-color: transparent;
border-radius: 50%;
animation: spin 0.9s linear infinite;
}
.logo-loader img { animation: breathe 1.8s ease-in-out infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes breathe { 50% { transform: scale(0.92); opacity: 0.8; } }
@media (prefers-reduced-motion: reduce) {
.logo-loader::after { animation-duration: 2.7s; }
.logo-loader img { animation: none; }
}
</style>
Ambas animaciones usan solo transform y opacity, y el tamaño fijo reserva el espacio para que nada se mueva. El alt vacío hace que los lectores de pantalla lean el texto "Loading…" en lugar de anunciar el logo.
¿Cuánto debe durar el loop? Ninguna guía establece una duración de loop. Lo importante es que sea continuo (el último fotograma enlaza con el primero), constante en lugar de acelerar y frenar, y lo bastante corto para repetirse varias veces durante una espera típica de 2 a 10 segundos. Nunca dejes que un pulso se convierta en destellos: WCAG 2.3.1 fija un límite de tres destellos en cualquier período de un segundo (W3C).
Movimiento reducido: conserva el indicador y elimina el movimiento extra. En el ejemplo, el anillo gira más despacio y el logo deja de pulsar, mientras el texto de estado sigue anunciando la espera. Para un loader a pantalla completa al iniciar la app, las reglas son distintas; consulta cómo convertir el ícono de tu app en una intro de inicio.
¿Cómo mostrar un personaje de marca durante la carga?
Un personaje es un indicador en loop con personalidad, así que pertenece al mismo rango de 2 a 10 segundos que un spinner. Funciona mejor en los momentos en que los usuarios ya esperan: al generar una imagen, enviar un mensaje o sincronizar una biblioteca.
Con un archivo de Rive, el loader es un estado de una máquina de estados (state machine) y no un recurso aparte. Asignas a un input numérico el valor del estado de ánimo de carga mientras se realiza el trabajo y disparas un trigger de éxito o de error cuando termina, así que el mismo personaje también muestra el resultado. En la web:
import { Rive } from "@rive-app/webgl2";
const r = new Rive({
src: "/character.riv",
canvas: document.querySelector("#mascot"),
stateMachine: "CharacterStateMachine",
autoplay: true,
onLoad: () => {
r.resizeDrawingSurfaceToCanvas();
runWithMascot(); // inputs are available once the file has loaded
},
});
function setMood(n) {
const mood = r.stateMachineInputs("CharacterStateMachine").find((i) => i.name === "mood");
if (mood) mood.value = n;
}
function fire(name) {
r.stateMachineInputs("CharacterStateMachine").find((i) => i.name === name)?.fire();
}
async function runWithMascot() {
setMood(5); // loading loop
await doWork().then(() => fire("success"), () => fire("error"));
setMood(0); // back to idle
}
Los archivos de Charmotion están pensados justo para esto: en un set generado a partir de tu propia mascota de marca, el mood 5 es el loop de carga (1.2 segundos, continuo) y el mood 3 es thinking (2.4 segundos), con triggers success y error que vuelven al estado de ánimo actual. Los logos 3D o con degradados se animan tal como son, sin redibujarse, así que el mismo enfoque sirve para un loader con logo. Los mismos estados también se exponen mediante un view model Character para Data Binding. Si la tarea es larga y medible, mantén una barra de progreso real junto al personaje.
¿Cómo mostrar un loader solo mientras hay trabajo en curso?
Vincula el loader a la solicitud, no a un temporizador, y agrega un breve retraso antes de mostrarlo. Como los usuarios no necesitan ningún indicador para acciones de menos de un segundo aproximadamente, un retraso de unos cientos de milisegundos a un segundo oculta el loader en las respuestas rápidas y evita un parpadeo molesto.
async function withLoader(region, task, delay = 600) {
region.setAttribute("aria-busy", "true");
const timer = setTimeout(() => region.classList.add("is-loading"), delay);
try {
return await task();
} finally {
clearTimeout(timer);
region.classList.remove("is-loading");
region.setAttribute("aria-busy", "false");
}
}
El bloque finally es importante: los errores y las cancelaciones también deben quitar el loader, o la pantalla seguirá cargando para siempre. En React, la misma idea es un estado pending que se activa antes de la solicitud y se limpia en finally, con el loader renderizado de forma condicional.
En plataformas nativas, los componentes integrados son la opción por defecto. SwiftUI tiene ProgressView() para un spinner indeterminado y ProgressView(value:) para una barra determinada; Jetpack Compose tiene CircularProgressIndicator y LinearProgressIndicator, con o sin valor de progreso. Usa una vista con personaje solo donde el momento de marca lo justifique, con el mismo indicador pending para mostrarla y ocultarla.
¿Cómo se hace accesible un loader?
Tres cosas hacen que un loader funcione para todos:
- Anúncialo. Pon un texto breve como "Cargando resultados…" dentro de un elemento con
role="status", una región dinámica que los lectores de pantalla leen de forma discreta sin mover el foco (MDN). Oculta el spinner o el personaje decorativo conaria-hidden="true". - Marca la región como ocupada. Define
aria-busy="true"en el contenedor que se está actualizando y vuelve afalsecuando termine, para que las tecnologías de asistencia puedan esperar al contenido final. - Respeta el movimiento reducido. La media query
prefers-reduced-motion: reducerefleja el ajuste de reducir movimiento del sistema operativo (MDN). Ralentiza el spinner, reemplaza un personaje que rebota por una pose estática con texto o muestra una barra de progreso sencilla. En SwiftUI, lee@Environment(\.accessibilityReduceMotion).
WCAG 2.2 SC 2.2.2 (Pausar, detener, ocultar) normalmente exige una forma de pausar el movimiento que dura más de cinco segundos junto a otro contenido. Indica que una animación durante una fase de precarga puede considerarse esencial cuando no es posible ninguna interacción en ese momento (W3C). Un loader junto a contenido que el usuario aún puede usar no entra en esa excepción, así que detenlo cuando termine el trabajo. Más información en animación accesible y movimiento reducido.
¿Cuáles son los errores más comunes en las animaciones de carga?
- Un loader para acciones instantáneas. Un spinner que aparece durante 150 ms parece un fallo. Retrásalo.
- Un spinner en una exportación de dos minutos. Pasados unos 10 segundos, los usuarios quieren saber cuánto falta. Muestra una barra con porcentaje o un contador de pasos.
- Bloqueos a pantalla completa para actualizaciones parciales. Si solo se está actualizando una lista, pon el indicador en la lista y deja el resto de la pantalla utilizable.
- Layout shift. Un loader que empuja el contenido hacia abajo al aparecer y luego lo devuelve a su sitio es peor que no tener ninguno. Reserva su espacio o muéstralo como overlay.
- Recursos de loader pesados. Un loader en GIF o video grande puede tardar más en descargarse que el trabajo que cubre. Un spinner en CSS son unas pocas líneas; un archivo de Rive pequeño suele pesar decenas de kilobytes.
- Estados atascados. Quita siempre el loader ante errores y tiempos de espera agotados, y reemplázalo por un mensaje claro y una opción para reintentar.
Si el personaje también muestra estados de éxito y error, consulta estados de animación de una mascota de marca para asociar eventos con reacciones.
Cómo agregar una animación de carga
- Estima la espera. Mide las duraciones típicas. Menos de 1 segundo aproximadamente no necesita loader; de 2 a 10 segundos, un indicador en loop; 10 segundos o más, una barra con porcentaje completado.
- Elige el indicador. Usa un skeleton cuando conozcas el diseño, un spinner o un loop de personaje para esperas cortas de duración desconocida y una barra de progreso para trabajo medible.
- Créalo con propiedades ligeras. Anima solo
transformyopacityen CSS, o usa un archivo de Rive pequeño con un estado de ánimo de carga para un personaje. - Vincúlalo a la solicitud. Muestra el loader tras un breve retraso mientras la solicitud está pendiente y quítalo en un bloque
finallypara que los errores también lo eliminen. - Hazlo accesible. Agrega texto con
role="status", alternaaria-busy, oculta el movimiento decorativo a los lectores de pantalla y ralentízalo o reemplázalo con movimiento reducido.
Preguntas frecuentes
¿Cuánto debo esperar antes de mostrar un spinner de carga?
Espera lo suficiente para que las respuestas rápidas nunca lo muestren. Nielsen Norman Group recomienda no mostrar ningún indicador en acciones de menos de un segundo aproximadamente, así que un retraso de entre unos cientos de milisegundos y un segundo funciona bien. Cancela siempre el retraso si la solicitud termina antes.
¿Es mejor un skeleton screen que un spinner?
Ninguno es mejor en todos los casos. Un skeleton funciona para contenido cuyo diseño ya conoces, como un feed o una cuadrícula de productos, porque la página parece completarse en su lugar. Un spinner funciona para acciones sin un diseño predecible. Muchas apps usan un skeleton en la primera carga y spinners después.
¿Puede una mascota de marca reemplazar una barra de progreso?
No en tareas largas. Una mascota en loop indica que se está trabajando, pero no cuánto falta. Úsala para esperas de unos 2 a 10 segundos y, a partir de 10 segundos, muestra una barra con el porcentaje completado, si quieres con la mascota al lado.
¿Cómo hago un spinner de carga con mi logo?
Coloca el logo, sin movimiento, en el centro de un anillo CSS que gire con transform, o dale al propio logo un pulso suave de escala y opacidad. Haz girar el logo completo solo si es redondo y simétrico. Agrega texto con role="status", oculta la imagen a los lectores de pantalla y ralentiza o detén el movimiento cuando se active el movimiento reducido.
¿Debe detenerse un loader con movimiento reducido?
Debe calmarse en lugar de desaparecer, porque los usuarios necesitan saber que hay trabajo en curso. Ralentiza la rotación, cambia un personaje que rebota por una pose estática o muestra una barra de progreso, y conserva el texto con role="status" para que el estado se siga anunciando.
Respuestas rápidas de esta guía
Fuentes
- Progress Indicators Make a Slow System Less Insufferable, Nielsen Norman Group
- Response Times: The 3 Important Limits, Nielsen Norman Group
- How to create high-performance CSS animations, web.dev
- ARIA: status role, MDN Web Docs
- aria-busy, MDN Web Docs
- prefers-reduced-motion, MDN Web Docs
- Understanding SC 2.2.2: Pause, Stop, Hide, W3C WAI
- Understanding SC 2.3.1: Three Flashes or Below Threshold, W3C WAI
- Web (JS) runtime, Rive
Guías relacionadas
- Duración de animaciones y UX de carga: pautas
- Estados de animación de mascota de marca: Loading, Success, Error
- Animación accesible: movimiento reducido y WCAG
- Cómo agregar una mascota de marca al onboarding y a los estados vacíos
- Cómo agregar un logo animado al header de tu página web
- Convierte el ícono de tu app en una intro de inicio