prefers-reduced-motion.Adding a loading spinner used to mean dropping a 12 KB animated GIF into your project. CSS animations made it weight-zero — a few hundred bytes of CSS, GPU-accelerated rendering, instantly responsive to dark mode and theme changes. The result: every modern app uses pure-CSS loaders. The challenge: writing a smooth, accessible loader from scratch involves keyframes, timing functions, transform-origin, and accessibility considerations that aren’t obvious.
Our CSS loader generator ships 60+ tested presets across five categories — spinning circles, dots, bars, skeleton screens, and conic-gradient loaders. Customise size, speed, colour, and stroke width with sliders. Copy production-ready CSS that includes prefers-reduced-motion handling and proper ARIA attributes. This guide covers when each loader type is appropriate, the accessibility traps, and the performance gotchas that turn a smooth spinner into a paint-thrashing battery drain.
Five loader types and when to use each
| Type | Best for | Avoid when |
|---|---|---|
| Spinning circle | Generic “loading…” (under 1s) | User waits more than 3s — feels unresponsive |
| Bouncing dots | Chat / typing indicators | Page-level loading — too playful |
| Progress bar | Known-duration tasks | Indeterminate progress (use indeterminate variant) |
| Skeleton screen | Content-heavy pages — feels faster than spinners | Single-element waits — overkill |
| Conic-gradient | Modern, distinctive loaders | IE / very old browser support required |
A canonical pure-CSS spinner
The simplest CSS spinner is a circle with a transparent top border, rotated indefinitely. About 8 lines of CSS, GPU-accelerated, ~200 bytes minified:
.loader {
width: 32px;
height: 32px;
border: 4px solid #e0e0e0;
border-top-color: #635BFF;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
.loader { animation-duration: 3s; }
}
The reduced-motion media query slows the animation rather than stopping it — users with vestibular disorders prefer slow over absent. Stopping animations entirely can break the implied “still loading” message.
How to build a loader in your browser
- Open the CSS loader generator
- Pick a preset from the gallery (60+ available across 5 categories)
- Adjust size, speed (animation duration), colour, and stroke width
- Watch the live preview animate in real time
- Click Copy CSS for production-ready code with
prefers-reduced-motionhandling - Or click Copy as HTML+CSS for a complete drop-in component
Accessibility — the loaders that beat off-the-shelf libraries
Most spinners shipped on Stack Overflow and CodePen miss accessibility basics:
- ARIA role + label. Loaders need
role="status"andaria-label="Loading"so screen readers announce them. Without these, blind users have no idea anything is happening. - prefers-reduced-motion. Slow or pause animations for users with vestibular disorders. Don’t remove the spinner — slow it from 0.8s to 3s rotation.
- Visible loading text. A pure visual spinner gives no information. Pair with text like “Loading”, “Saving…”, “Fetching results” — both for accessibility and to communicate context.
- Removed from DOM when done. An invisible spinner left in the DOM still gets announced by screen readers as “loading”. Remove it or set
aria-hidden="true"when complete. - Don’t trap focus. A loader covering the page shouldn’t trap keyboard focus inside the spinner. Either make the page scrim non-interactive or move focus to a meaningful element when loading completes.
Our generator outputs CSS with the ARIA attributes baked in. Copy and paste — accessibility done.
Common gotchas
- Don’t animate
top,left, orwidth. Layout-triggering properties cause the browser to recalculate every frame. Always animatetransformandopacityfor 60fps. Our presets only use those. - Conic gradients need fallbacks.
conic-gradient()works in Chrome 69+, Safari 13+, Firefox 83+ — close to universal in 2026. For very old browsers (IE, very old Android stock browser), provide a fallback border-style spinner via@supports. - Skeleton screens with shimmer animation can be expensive. A full-page skeleton with 20 animated rectangles taxes mobile CPUs. Test on a low-end phone before shipping; consider a single-element pulse instead of per-element shimmer.
- Animation timing functions matter.
linearfor spinners (constant rotation feels “loading”),ease-in-outfor bouncing dots (more natural pulse),cubic-bezier()for character or branded loaders. - Don’t stack multiple loaders on one page. One global spinner is enough; multiple loaders create competing animations that distract. Use skeleton screens for multiple regions instead.
- Border-radius doesn’t always match expected geometry. A 50% border-radius on a non-square element produces an ellipse, not a circle. Spinners need explicit equal width and height.
When NOT to use a CSS loader
For known-duration uploads or downloads, use a real progress bar showing percentage — much more informative than an indeterminate spinner. For sub-100ms operations, don’t show a loader at all (it’ll flash and feel jankier than no feedback). For very long operations (over 10 seconds), pair with text updates (“Uploading… 1.2 MB of 4 MB”) because a constant spinner stops feeling like progress. For complex character-style loaders (like the Discord cat or GitHub Octocat), consider an SVG or Lottie animation instead — pure CSS hits a complexity ceiling around morphing shapes.
Frequently asked questions
Are CSS loaders accessible to screen readers?
Only with role="status" and aria-label. Our presets ship with both. A bare <div> with animation gives no information to assistive tech — always pair the visual with proper ARIA, plus a hidden text alternative if the loader represents a complex state.
Do CSS loaders work on all browsers?
Border-style spinners and bouncing-dot loaders work in every browser since IE 10. Conic-gradient loaders need Chrome 69+, Safari 13+, Firefox 83+ — universal in 2026 except for IE (retired by Microsoft in 2022). Use @supports (background: conic-gradient(red, blue)) for graceful fallback if you support IE.
How do I respect prefers-reduced-motion?
Add a media query that slows or replaces the animation: @media (prefers-reduced-motion: reduce) { .loader { animation-duration: 3s; } }. Don’t remove the loader entirely — users still need feedback that something is happening.
Can I use these in React, Vue, or Svelte?
Yes — they’re CSS-only, so framework-agnostic. Drop the CSS into a stylesheet, add the markup as a component, and apply the class. Use scoped styles in Vue/Svelte to avoid cross-component conflicts.
Will animated loaders drain the battery?
Properly built ones (animating only transform and opacity) are GPU-accelerated and very cheap. Layout-triggering animations (animating width or top) can drain battery. Browsers also pause animations on hidden tabs to save power; a forgotten spinner on an inactive tab won’t keep the GPU busy.
Is my data uploaded?
No. The generator runs entirely in your browser. Customisations and the generated CSS stay on your device.
Related tools and guides
- CSS Loader Generator
- Cubic Bezier Generator (for loader timing)
- CSS Gradient Generator
- CSS Glassmorphism Generator
- All CSS tools
