Un spinner de carga no necesita JavaScript. Ni una librería, ni un GIF, ni Lottie: con un div y unos keyframes obtienes indicadores de carga nítidos en cualquier resolución, con coste de rendimiento mínimo. Este artículo desmonta las técnicas detrás de los seis spinners más usados para que entiendas qué hace cada pieza.
La técnica madre: el anillo de border
El spinner clásico (el de GitHub, Bootstrap o YouTube) explota una propiedad de CSS que pocos tienen presente: los cuatro bordes de un elemento se dibujan por separado. Haz un elemento circular con borde grueso y vuelve transparente uno de los lados:
.loader {
width: 48px;
height: 48px;
border: 4px solid rgba(124, 58, 237, 0.2);
border-bottom-color: #7c3aed;
border-radius: 50%;
animation: loader-rotation 1s linear infinite;
}
@keyframes loader-rotation {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
Con el markup <div class="loader"></div> obtienes un aro con una pista tenue completa y un arco de color vivo en la parte inferior. Al rotar el elemento entero, ese arco recorre los 360 grados y produce la ilusión de giro continuo. Tres decisiones de diseño dentro de estas líneas:
linear: la rotación debe ser de velocidad constante. Coneaseel spinner "respiraría", acelerando y frenando.- Pista translúcida: el mismo color con alpha bajo (notación hexadecimal de 8 dígitos,
#7c3aed33) muestra dónde está el anillo completo y transmite progreso percibido. - Animar
transform, no propiedades visuales:transformse compone en GPU sin disparar reflow. Animarborder-colorowidthobliga al navegador a recalcular layout.
La variante "doble anillo" es la misma idea con el borde entero del color pleno excepto border-bottom-color: transparent, lo que crea un hueco visible que hace más obvio el movimiento.
Puntos escalonados: delays con nth-child
Los tres puntos rebotando en secuencia (el patrón de "escribiendo..." de las apps de mensajería) usan tres divs hermanos con la misma animación retardada de forma progresiva:
.dots {
display: flex;
gap: 6px;
}
.dots div {
width: 12px;
height: 12px;
border-radius: 50%;
background: #7c3aed;
animation: loader-bounce 0.6s ease-in-out infinite;
}
.dots div:nth-child(2) { animation-delay: 0.1s; }
.dots div:nth-child(3) { animation-delay: 0.2s; }
@keyframes loader-bounce {
0%, 100% { transform: scale(0.6); opacity: 0.5; }
50% { transform: scale(1); opacity: 1; }
}
El escalonado crea la percepción de secuencia u onda viajera. Con cinco barras en vez de tres puntos y una animación de scaleY(), tienes la variante "barras ecualizador".
El truco del delay negativo
Aquí hay un detalle de animation-delay que casi nadie conoce y resuelve un problema real. Imagina dos ondas circulares expandiéndose (el efecto "radar" o "ripple"). Quieres que estén desfasadas medio ciclo, pero también que la segunda onda ya esté en marcha desde el primer frame, sin esperar su delay inicial.
El delay positivo falla: durante el primer medio segundo la segunda onda está estática. La solución es un delay negativo:
.ripple::before,
.ripple::after {
content: "";
position: absolute;
inset: 0;
border: 4px solid #7c3aed;
border-radius: 50%;
animation: loader-ripple 1.5s ease-out infinite;
}
.ripple::after {
animation-delay: -0.75s;
}
@keyframes loader-ripple {
from { transform: scale(0); opacity: 1; }
to { transform: scale(1); opacity: 0; }
}
Un delay negativo significa "empieza la animación como si ya llevara X segundos ejecutándose": el navegador salta directamente al punto correspondiente del timeline. Es la diferencia entre una animación que arranca coja y otra que parece llevaba siempre ahí.
Pseudoelementos para no ensuciar el HTML
Fíjate en que el ripple usa ::before y ::after: dos ondas con un único div. Para loaders pequeños esto importa: menos nodos en el DOM, markup limpio (<div class="ripple"></div>) y estilos autocontenidos. Los pseudoelementos son hijos renderizados completos, capaces de recibir animaciones propias, posicionamiento absoluto y bordes.
Nombres de keyframes con namespace
Cuando copias un spinner a tu proyecto, sus keyframes conviven con los del resto de la aplicación. Un keyframe llamado spin colisionará con cualquier otro spin. La práctica segura es prefijarlos según el componente: loader-rotation, loader-bounce, loader-pulse. Evita el clásico bug de "un spinner roto misteriosamente en otra página".
Rendimiento y accesibilidad
Dos apuntes que separan un loader profesional de uno copiado de un tutorial:
Rendimiento: todos los ejemplos anteriores animan únicamente transform y opacity, las dos propiedades que el navegador compone en GPU sin recalcular layout ni paint. Un spinner que anima width, height o margin provoca reflow en cada frame.
Accesibilidad: un spinner puramente visual es invisible para lectores de pantalla. Añade contexto semántico:
<div class="loader" role="status" aria-live="polite">
<span class="sr-only">Cargando...</span>
</div>
Además, respeta prefers-reduced-motion: con esa media query puedes sustituir el giro por un pulso de opacidad o mostrar texto plano.
Genera los seis tipos configurados
Elegir tipo, color, tamaño y duración, verlo en vivo y copiar HTML + CSS juntos es lo que hace nuestro generador de Loaders CSS: anillo, doble anillo, puntos, barras, pulso y ondas, con el código autocontenido listo para pegar en tu proyecto.
Preguntas frecuentes
¿Por qué no usar un GIF o Lottie? Un GIF pesa, se pixela y no admite color dinámico. Lottie es excelente para animaciones complejas pero exige librería y bundle extra. Para indicar carga, CSS es más ligero, nítido y personalizable en runtime.
¿Cuánto debe durar cada ciclo? Entre 0,6 y 1,2 segundos es el rango habitual. Por debajo de 0,5s genera ansiedad; por encima de 2s parece que algo se ha colgado.
¿Se puede pausar un loader desde JavaScript? Sí: element.getAnimations() te devuelve las animaciones activas y puedes llamar a .pause() o .play() sobre ellas sin tocar el CSS.
Genera tu spinner con vista previa usando el generador de Loaders CSS, gratis y directamente en tu navegador.