Keyframe
Keyframe · The log

CSS Loading Animation: Spinners, Bars, and Dots Without a JavaScript Library

July 24, 2026 · Uncategorized

A loading animation buys you patience. The work behind the screen takes however long it takes, but a small spinning thing tells the visitor the page has not died, and that alone keeps people from hitting refresh. You do not need a library for any of it. Three shapes cover almost every case: a spinner, a bar, and a row of dots.

The CSS spinner everyone starts with

The classic ring spinner is a circle with one edge colored in, rotating forever. Border does the drawing, border-radius: 50% makes it round, and a single keyframe handles the spin.

.spinner {
  width: 40px;
  height: 40px;
  border: 4px solid #2a2a2a;
  border-top-color: #ff3d7f;
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

Two details carry the effect. The timing function is linear, because a spinner that eases looks like it is struggling, and it repeats infinite since you never know how long the wait runs. Speed matters more than people expect. Around 0.8 seconds per turn reads as working. Drop below half a second and it looks frantic, which quietly makes the wait feel worse.

An indeterminate progress bar

When you cannot measure real progress, a sliding bar fills the same job. A thin track holds a short colored segment that runs across and repeats.

.bar {
  height: 4px;
  width: 100%;
  background: #1a1a1a;
  overflow: hidden;
}

.bar::after {
  content: "";
  display: block;
  width: 40%;
  height: 100%;
  background: #14e0c3;
  animation: slide 1.2s ease-in-out infinite;
}

@keyframes slide {
  from { transform: translateX(-100%); }
  to   { transform: translateX(350%); }
}

Here easing helps, unlike the spinner, because a bar that speeds up and slows down feels alive rather than mechanical. Animating transform instead of left keeps it on the GPU and off the main thread, which is the difference between buttery and janky on a cheap phone. The reasons behind that choice live in the note on CSS animation performance.

Bouncing dots

Three dots that bounce in sequence read as “typing” or “thinking,” and they are just one animation offset by a staggered delay.

.dots span {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #ff3d7f;
  animation: bounce 1s ease-in-out infinite;
}

.dots span:nth-child(2) { animation-delay: 0.15s; }
.dots span:nth-child(3) { animation-delay: 0.3s; }

@keyframes bounce {
  0%, 60%, 100% { transform: translateY(0); }
  30%           { transform: translateY(-8px); }
}

The trick is the animation-delay on the second and third dot. Same keyframes, shifted start, and the wave appears on its own. If the staggering or the percentages feel like guesswork, the guide to CSS keyframes walks through reading them.

When to skip the animation entirely

Motion is for real waits. If something resolves in under a few hundred milliseconds, a spinner that flashes on and off looks like a glitch, so let fast things stay still. Reserve the loader for jobs that take a second or more, where the reassurance is worth it.

One more thing that gets forgotten: some people get motion sick from looping animation. Wrap anything that spins forever in a prefers-reduced-motion check and swap it for a static state when asked, a habit covered in accessible animation. For the wider set of techniques these loaders draw on, start at the pillar on CSS animation.

← Back to the log