CSS Loading Spinner Tutorial: Pure CSS Spinners and Loader Animations
CSS Loading Spinner Tutorial: Pure CSS Spinners and Loader Animations
A css loading spinner is one of the most practical UI components you can build without JavaScript. Pure CSS spinners are lightweight, render instantly, and work even when JavaScript fails to load. This tutorial walks through four complete pure css spinner patterns — from the classic spinning border to bouncing dots — with accessibility best practices included.
Why Use Pure CSS Spinners?
Before reaching for a JavaScript library or an animated GIF, consider the advantages of a pure css spinner:
- Zero JavaScript — works before any scripts parse or execute
- Tiny footprint — typically under 20 lines of CSS
- Smooth animation — CSS animations use the browser’s compositor for 60fps performance
- Easy theming — change one color variable to match your brand
- Accessible by default — easy to pair with
aria-labelfor screen readers
The Core Technique: Spinning Border
The most common css loading spinner uses a circular element with a partially transparent border and the @keyframes rotate animation:
.spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(99, 102, 241, 0.2);
border-top-color: #6366f1;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
The key insight: three sides of the border are transparent; one side is solid. As the element rotates, only the solid side is visible — creating the illusion of a spinning arc.
4 Complete Copy-Paste CSS Loaders
Loader 1: Classic Spinning Border
Clean and universal — works on any background:
.loader-spin {
display: inline-block;
width: 40px;
height: 40px;
border: 4px solid rgba(99, 102, 241, 0.15);
border-top-color: #6366f1;
border-radius: 50%;
animation: spin 0.75s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
Loader 2: Dual Ring
Two rings rotate in opposite directions for a more dynamic look:
.loader-dual {
position: relative;
width: 48px;
height: 48px;
}
.loader-dual::before,
.loader-dual::after {
content: '';
position: absolute;
inset: 0;
border-radius: 50%;
border: 4px solid transparent;
}
.loader-dual::before {
border-top-color: #6366f1;
animation: spin 0.8s linear infinite;
}
.loader-dual::after {
border-bottom-color: #ec4899;
animation: spin 0.8s linear infinite reverse;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
Loader 3: Bouncing Dots
Three dots that scale up in sequence — great for “loading” states in chat or AI interfaces:
.loader-dots {
display: flex;
gap: 6px;
align-items: center;
}
.loader-dots span {
display: block;
width: 10px;
height: 10px;
background: #6366f1;
border-radius: 50%;
animation: bounce 1.2s ease-in-out infinite;
}
.loader-dots span:nth-child(2) {
animation-delay: 0.2s;
}
.loader-dots span:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes bounce {
0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; }
40% { transform: scale(1); opacity: 1; }
}
HTML for this pure css spinner:
<div class="loader-dots" role="status" aria-label="Loading">
<span></span>
<span></span>
<span></span>
</div>
Loader 4: Pulse Ring
A ring that expands and fades — elegant for overlay loading states:
.loader-pulse {
position: relative;
width: 48px;
height: 48px;
}
.loader-pulse::before,
.loader-pulse::after {
content: '';
position: absolute;
inset: 0;
border-radius: 50%;
border: 3px solid #6366f1;
animation: pulse-ring 1.5s ease-out infinite;
}
.loader-pulse::after {
animation-delay: 0.75s;
}
@keyframes pulse-ring {
0% { transform: scale(0.5); opacity: 1; }
100% { transform: scale(1.5); opacity: 0; }
}
How to Center a Loader
Center in Viewport (Full-page overlay)
.loader-overlay {
position: fixed;
inset: 0;
background: rgba(255, 255, 255, 0.85);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
Center Inside a Container
.card-loading {
position: relative;
min-height: 200px;
}
.card-loading .loader-spin {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
Accessibility for CSS Loaders
A spinning animation alone communicates nothing to screen reader users. Always add semantic markup:
<!-- Option 1: role="status" with aria-label -->
<div class="loader-spin" role="status" aria-label="Loading, please wait"></div>
<!-- Option 2: visually hidden text -->
<div class="loader-spin" role="status">
<span class="sr-only">Loading...</span>
</div>
The .sr-only class hides text visually but keeps it accessible:
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0,0,0,0);
border: 0;
}
Respecting prefers-reduced-motion
Some users configure their OS to reduce motion because animations cause discomfort. Honor this preference:
@media (prefers-reduced-motion: reduce) {
.loader-spin,
.loader-dual::before,
.loader-dual::after {
animation: none;
border-color: #6366f1; /* Show full circle instead */
}
.loader-dots span {
animation: none;
opacity: 1;
transform: scale(1);
}
}
This is an important accessibility consideration for any css loading spinner — the css loader animation should never be distracting for users who prefer reduced motion.
Customizing Colors
All four loaders use #6366f1 (indigo) as the primary color. To theme them for your brand, simply swap the color value. Using a CSS custom property makes it trivial:
:root {
--spinner-color: #10b981; /* emerald green */
}
.loader-spin {
border-top-color: var(--spinner-color);
border-color: color-mix(in srgb, var(--spinner-color) 15%, transparent);
}
Summary
A css loading spinner is one of the fastest UI wins available in pure CSS. The spinning border technique is the foundation — just a border-radius: 50% element, a partially transparent border, and @keyframes rotate. Layer on accessibility with role="status", and always include a prefers-reduced-motion override. The four pure css spinner patterns above cover the most common loading state scenarios you will encounter.
Use our free CSS Loader Generator to pick, customize, and download loading animations without writing any code.
Generate this CSS visually — no coding required. Instant live preview.
Open Tool →