A CSS loading animation is one of the most commonly needed UI components — and also one of the most over-engineered. Most use cases need just one HTML element, 8–15 lines of CSS, and zero JavaScript. This guide covers the 12 most useful CSS loader patterns, how to build them from scratch, accessibility requirements, reduced-motion support, and how to use them in React, Vue, and Svelte.
Want to skip the theory and generate code? Use our CSS Loader Generator to customize any pattern visually and copy the code in one click.
The CSS Spinner: Foundation Pattern
The spinner is the industry default for loading states. It's built from a single element using the border trick — three transparent sides and one colored side create an arc that appears to rotate:
.spinner {
width: 48px;
height: 48px;
border: 4px solid rgba(124, 111, 255, 0.2); /* track */
border-top-color: #7c6fff; /* arc */
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
The transparent rgba border creates a visible "track" so the arc appears to move against a background. For a thinner track, use an even lower alpha value. For no track at all, use border-color: transparent on three sides.
Ring Loader (Dual-Line Variant)
A ring loader colors the top and bottom sides, creating two arc segments that chase each other around the circle:
.ring {
width: 48px;
height: 48px;
border: 4px solid transparent;
border-top-color: #7c6fff;
border-bottom-color: #7c6fff;
border-radius: 50%;
animation: spin 0.9s linear infinite;
}
Bouncing Dots
Three dots that scale in and out in sequence. The key is using negative animation delays so the animation appears to already be in progress when the page loads — without negative delays, all three dots start at the same time, creating a single-pulse illusion:
.dots {
display: flex;
gap: 8px;
align-items: center;
}
.dots div {
width: 12px;
height: 12px;
border-radius: 50%;
background: #7c6fff;
animation: bounce 1.2s ease-in-out infinite both;
}
.dots div:nth-child(1) { animation-delay: -0.32s; }
.dots div:nth-child(2) { animation-delay: -0.16s; }
@keyframes bounce {
0%, 80%, 100% { transform: scale(0); }
40% { transform: scale(1); }
}
Bar / Equalizer Loader
Four or five vertical bars that scale on the Y axis, creating an audio equalizer effect. Perfect for data loading and music players:
.bars {
display: flex;
gap: 4px;
align-items: center;
height: 48px;
}
.bars div {
width: 6px;
height: 100%;
background: #7c6fff;
border-radius: 2px;
animation: bars 1.2s ease-in-out infinite;
}
.bars div:nth-child(1) { animation-delay: -0.45s; }
.bars div:nth-child(2) { animation-delay: -0.3s; }
.bars div:nth-child(3) { animation-delay: -0.15s; }
@keyframes bars {
0%, 80%, 100% { transform: scaleY(0.4); }
40% { transform: scaleY(1); }
}
Ripple Loader
Two concentric circles that expand outward from the center and fade out, like a drop in water. Uses absolute positioning so both circles overlap:
.ripple {
position: relative;
width: 48px;
height: 48px;
}
.ripple div {
position: absolute;
inset: 0;
border-radius: 50%;
border: 3px solid #7c6fff;
animation: ripple 1s ease-out infinite;
}
.ripple div:nth-child(2) {
animation-delay: 0.5s;
}
@keyframes ripple {
0% { transform: scale(0); opacity: 1; }
100% { transform: scale(1); opacity: 0; }
}
Generate CSS loaders visually
12 styles, customizable color, size and speed. Copy HTML + CSS in one click.
Accessibility: The Part Most Developers Skip
A CSS loader is invisible to screen readers unless you explicitly add ARIA. A user with a screen reader navigating a page with a spinning div will hear nothing — they won't know a process is running. This is an accessibility failure.
The Correct Pattern
<div role="status" aria-label="Loading">
<div class="css-loader" aria-hidden="true"></div>
<span class="visually-hidden">Loading...</span>
</div>
/* CSS for the hidden label */
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
role="status" announces the element's content to screen readers politely (it waits for the user to finish their current task). Use role="alert" for urgent messages that should interrupt. The aria-hidden="true" on the visual spinner prevents it from being announced separately. The .visually-hidden text provides the accessible label.
Live region tip: When the loading process completes, update the status element's text to "Loading complete" or remove it. This announces completion to screen reader users, closing the loop on the interaction.
Reduced Motion Support
Some users have prefers-reduced-motion: reduce set in their OS (System Preferences → Accessibility on Mac, Display → Reduce Motion on iOS). For people with vestibular disorders, spinning and pulsing animations can cause real physical discomfort. Always provide a fallback:
/* Default — animated loader */
.css-loader {
animation: spin 0.8s linear infinite;
}
/* Reduced motion — simple fade instead */
@media (prefers-reduced-motion: reduce) {
.css-loader {
animation: none;
opacity: 0.6;
/* Or: replace with a static indicator */
}
}
Using CSS Loaders in React
CSS loaders work identically in React — just use className instead of class and import your CSS file or module:
// Spinner.jsx
import './Spinner.css';
function Spinner({ label = 'Loading' }) {
return (
<div role="status" aria-label={label}>
<div className="css-loader" aria-hidden="true" />
<span className="visually-hidden">{label}</span>
</div>
);
}
export default Spinner;
For dynamic color or size, use inline styles with CSS custom properties:
// Override custom properties per-instance
<div
className="css-loader"
style={{ '--loader-color': brandColor, '--loader-size': '24px' }}
/>
Using CSS Loaders in Vue
<template>
<div role="status" :aria-label="label">
<div class="css-loader" aria-hidden="true" />
<span class="visually-hidden">{{ label }}</span>
</div>
</template>
<script setup>
const props = defineProps({ label: { default: 'Loading' } });
</script>
<style scoped>
.css-loader {
width: 48px;
height: 48px;
border: 4px solid rgba(124,111,255,0.2);
border-top-color: #7c6fff;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
</style>
Choosing the Right Loader for the Context
- Full-page loading — Centered spinner or pulse, large size (48–64px), semi-transparent overlay
- Button loading state — Small spinner (16–20px) inline with button text, or replace text with spinner
- Inline/skeleton replacement — Pulse animation on a gray rectangle matching the content shape
- Data processing / audio — Bar/equalizer loader to suggest active processing
- Network activity — Dots loader or ripple for softer, "waiting" feel
Frequently Asked Questions
Should I use a CSS loader or a GIF/SVG animation?
How do I show/hide a CSS loader with JavaScript?
.hidden { display: none; } to your CSS, then spinner.classList.add('hidden') when done. Alternatively, toggle visibility: hidden; opacity: 0; with a transition for a fade-out effect. Don't just set display: none directly — it's hard to transition.Can I use CSS loaders in a button?
position: relative and use an ::after pseudo-element for the spinner, or include a small spinner element inside the button HTML. When loading, disable the button with disabled or pointer-events: none to prevent double-submit.What size should a CSS loader be?
Ready to generate a loader with the exact color and style you need? Use the CSS Loader Generator. For more animation techniques, see the CSS Animation Generator and the CSS animation performance guide.