Tema
Loaders y spinners
Cuando algo tarda, el usuario necesita saber que la página no se ha congelado. Un buen indicador de carga reduce la ansiedad y hace que la espera se sienta más corta. Aquí tienes 15 variantes en CSS puro.
Concepto: animaciones infinitas y animation-delay negativo
Los loaders se basan en tres ideas:
- Bucles infinitos:
animation-iteration-count: infinite. - Curva
linearpara giros: si un spinner usaease, acelera y frena en cada vuelta y parece "cojear". - Desfase entre piezas con
animation-delay. Un truco profesional es usar retrasos negativos:animation-delay: -.4shace que la animación empiece como si ya llevara 0.4 s en marcha, así todas las piezas se mueven desde el primer fotograma.
css
.spinner {
width: 48px; height: 48px; border-radius: 50%;
border: 5px solid rgb(139 92 246 / .2); /* aro tenue */
border-top-color: #8b5cf6; /* un solo lado de color */
animation: girar 1s linear infinite;
}
@keyframes girar { to { transform: rotate(360deg); } }12 loaders CSS
Spinner, anillo doble, puntos, "escribiendo…", barras, pulso, órbita, flip, cónico, cuadrícula, reloj de arena y latido. Todos se personalizan con las variables --color, --color-2, --tam y --vel.
Algunas técnicas destacadas:
- Anillo doble:
::beforey::aftergiran en sentidos opuestos conanimation-direction: reverse. - Barras: se anima
transform: scaleY(), nuncaheight(más fluido). - Cónico: un
conic-gradientrecortado conmask: radial-gradient(...)para dejar sólo un aro con degradado. - Cuadrícula: cada celda recibe un
--d(diagonal) y el retraso escalc(var(--d) * .1s), creando una ola en diagonal. - Latido: un cuadrado rotado 45° más dos círculos (
::beforey::after) forman el corazón.
Accesibilidad en loaders
- Añade
role="status"yaria-label="Cargando"para que los lectores de pantalla anuncien el estado. - Con
prefers-reduced-motion, en lugar de congelar el loader (perdería su significado), lo hacemos mucho más lento::root { --vel: 3s; }.
Skeleton y barras de progreso
El skeleton (esqueleto) muestra la forma del contenido antes de que llegue. Plataformas como YouTube, LinkedIn o Facebook lo usan porque se percibe más rápido que un spinner. Las barras de progreso son ideales cuando sabes cuánto falta (subidas de archivos, pasos de un formulario).
Skeleton con brillo (shimmer):
css
.sk {
background: linear-gradient(90deg, #e2e8f0 25%, #f8fafc 50%, #e2e8f0 75%);
background-size: 200% 100%;
animation: shimmer 1.4s linear infinite;
border-radius: 6px;
}
@keyframes shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }Barra de progreso accesible: usa role="progressbar" con aria-valuenow, aria-valuemin y aria-valuemax, y actualiza el valor desde JS. También existe el elemento nativo <progress>, más sencillo pero más difícil de estilizar.
¿Spinner, skeleton o barra?
| Situación | Mejor opción |
|---|---|
| Espera corta (menos de 1 s) | Nada, o un spinner pequeño en el botón |
| Carga de una página o lista | Skeleton |
| Sabes el porcentaje (subida, instalación) | Barra determinada |
| No sabes cuánto falta | Barra indeterminada o spinner |
Mostrar y ocultar un loader en un proyecto real
js
const loader = document.querySelector('.spinner');
const contenido = document.querySelector('#contenido');
async function cargarProductos() {
loader.hidden = false; // mostrar
try {
const res = await fetch('/api/productos');
const datos = await res.json();
contenido.innerHTML = datos.map((p) => `<li>${p.nombre}</li>`).join('');
} catch (err) {
contenido.textContent = 'No se pudieron cargar los productos.';
} finally {
loader.hidden = true; // ocultar siempre, haya error o no
}
}Error común: loaders que parpadean
Si la petición tarda 100 ms, el loader aparece y desaparece en un parpadeo molesto. Muestra el loader sólo si la carga supera ~300 ms con un setTimeout que cancelas al terminar.
Ejercicios
- Crea un spinner de 3 aros concéntricos que giren a distintas velocidades.
- Convierte el loader de "puntos" en un indicador de "escribiendo…" dentro de una burbuja de chat.
- Crea un skeleton para una tarjeta de producto (imagen, título, precio y botón).
Ver solución del ejercicio 2
html
<div class="burbuja"><div class="escalar"><i></i><i></i><i></i></div></div>css
.burbuja {
display: inline-flex; padding: 12px 16px;
background: #e2e8f0; border-radius: 18px 18px 18px 4px;
}
.escalar i { width: 8px; height: 8px; background: #64748b; }Resumen
- Giros:
linear+infinite. Pasos de varias piezas:animation-delay(también negativo). - Anima
transform(rotate,scale), nowidth/height. - Skeleton = degradado +
background-positionanimado. role="status"/role="progressbar"para accesibilidad; con reduced-motion, ralentiza en vez de eliminar.