cssloaderspinneranimaciones css

Spinners de carga solo con CSS: la técnica border y sus variantes

Cómo crear loaders CSS sin JavaScript: el truco del border giratorio, keyframes, delays escalonados con nth-child, delay negativo y notas de rendimiento.

23 de agosto de 2026·7 min de lectura

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. Con ease el 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: transform se compone en GPU sin disparar reflow. Animar border-color o width obliga 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.

Pruébalo sin código

Generador de Loaders CSS

Spinners animados en CSS puro.

Abrir Generador de Loaders CSS

Hecho por

Miguel Ángel Colorado Marin (MACM)

Full-Stack Developer · Guadalajara, España

Desarrollo aplicaciones web, herramientas digitales y proyectos completos — desde el diseño hasta el despliegue.

Contáctame