How to Create a Loading Spinner in CSS

Build a spinning loader animation with pure CSS to show that content is loading in the background.

What a spinner communicates

A spinner tells the visitor "something is happening, please wait" when there is no meaningful percentage to show - unlike a progress bar, which needs a known value, a spinner just needs to keep moving for as long as the wait lasts. The classic implementation is a circle whose border is transparent on one side, rotated continuously with a CSS `@keyframes` animation.

Building the spinning circle

Start with a plain circle: a fixed-size box with `border-radius: 50%` and a solid `border`. Then make one side of that border a different color (or transparent) so the circle looks like it has a gap. Finally, spin the whole element with `@keyframes` that rotates it from 0 to 360 degrees, repeating forever.

Example: CSS spinner

<!DOCTYPE html>
<html>
<head>
<style>
  .spinner {
    width: 48px;
    height: 48px;
    border: 6px solid #e0e0e0;      /* light track color on every side */
    border-top-color: #2563eb;      /* one side stands out */
    border-radius: 50%;
    animation: spin 0.8s linear infinite;
  }

  @keyframes spin {
    from { transform: rotate(0deg); }
    to   { transform: rotate(360deg); }
  }
</style>
</head>
<body>

<div class="spinner" role="status" aria-label="Loading"></div>

</body>
</html>

The illusion of movement comes entirely from `border-top-color` being different from the other three sides. As the whole circle rotates, that highlighted arc sweeps around, and because the animation loops with `infinite` and uses `linear` timing, the motion never pauses or eases at a corner.

Note: Use linear timing, not ease-in-out, for continuous rotations. Easing makes the spinner appear to speed up and slow down each lap, which reads as stuttering rather than smooth spinning.

A dual-ring variant

For a slightly richer look, layer a second ring inside the first using a `::after` pseudo-element and spin it in the opposite direction at a different speed. This is purely a visual variation - the underlying technique (transparent/contrasting border side + rotate keyframes) is identical.

Example: dual-ring spinner

<!DOCTYPE html>
<html>
<head>
<style>
  .spinner-dual {
    position: relative;
    width: 48px;
    height: 48px;
  }
  .spinner-dual::before,
  .spinner-dual::after {
    content: "";
    position: absolute;
    border-radius: 50%;
    border: 4px solid transparent;
  }
  .spinner-dual::before {
    inset: 0;
    border-top-color: #2563eb;
    animation: spin 1s linear infinite;
  }
  .spinner-dual::after {
    inset: 8px;
    border-bottom-color: #93c5fd;
    animation: spin 1.4s linear infinite reverse;
  }
  @keyframes spin {
    to { transform: rotate(360deg); }
  }
</style>
</head>
<body>

<div class="spinner-dual"></div>

</body>
</html>
  • Keep the animation on transform, not on properties like top/left, so the browser can run it on the compositor thread for smooth performance.
  • Add role="status" and an aria-label so screen reader users are told that content is loading.
  • Remove the spinner from the DOM (or hide it) as soon as the real content is ready, rather than leaving it spinning forever.
Note: Respect the prefers-reduced-motion media query for visitors sensitive to motion: wrap the animation in @media (prefers-reduced-motion: no-preference) or provide a slower, less intense fallback.

Exercise: Feedback and Overlays

What is required to show and hide a modal built with the native dialog element's modal mode?

Frequently Asked Questions

How do I make a loading spinner with pure CSS?
Make a square div with border-radius: 50%, give it a light border on all sides and a darker border on one, then rotate it forever with a keyframe animation. No image or JavaScript is needed.
How do I change the speed of a CSS spinner?
Change the animation duration. animation: spin 1s linear infinite is a common starting point; lower it to speed up. Keep linear timing, because an eased rotation makes the spinner appear to stutter each turn.