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.
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.
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 infiniteis a common starting point; lower it to speed up. Keeplineartiming, because an eased rotation makes the spinner appear to stutter each turn.