How to Create a CSS Loading Spinner (No JavaScript)
Loading indicators tell users something is happening. A well-designed spinner reduces perceived wait time and prevents users from clicking away. Pure CSS loaders require no JavaScript, load instantly, and work even if scripts fail — making them ideal for initial page loads and lazy-loaded content.
- Build pure CSS loading spinners, dots, bars, and skeleton screens.
- The Classic Spinner.
- Covers bouncing dot loaders.
- Covers skeleton screens.
- Covers accessibility considerations.
The Classic Spinner
The simplest CSS spinner is a circle with a partial border that rotates:
.spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(255,255,255,0.1);
border-top-color: #00FFD1;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
This pattern is lightweight, accessible, and works in every browser. Customize the size, border width, and colors to match your design system.
Bouncing Dot Loaders
Three dots with staggered animations create a typing or loading indicator:
transform and opacity for smooth 60fps performance. These properties are handled by the GPU compositor and skip expensive layout recalculations..dots span {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
background: #00FFD1;
animation: bounce 1.4s ease-in-out infinite;
}
.dots span:nth-child(2) { animation-delay: 0.16s; }
.dots span:nth-child(3) { animation-delay: 0.32s; }
@keyframes bounce {
0%, 80%, 100% { transform: scale(0); }
40% { transform: scale(1); }
}
Dot loaders feel more conversational — they're commonly used in chat interfaces and messaging apps.
Skeleton Screens
Skeleton screens show a placeholder layout that mimics the content's shape before it loads. They reduce perceived load time more than spinners because users can see the content structure forming:
width, height, top, or left. These trigger layout recalculations on every frame and can drop performance below 60fps..skeleton {
background: linear-gradient(90deg, rgba(255,255,255,0.03) 25%, rgba(255,255,255,0.06) 50%, rgba(255,255,255,0.03) 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 8px;
}
@keyframes shimmer { to { background-position: -200% 0; } }
Apply this to placeholder divs that match the dimensions of your actual content elements.
Accessibility Considerations
Add role='status' and an aria-label to your loader container so screen readers announce the loading state. Use prefers-reduced-motion to provide a static alternative for users who've disabled animations:
@media (prefers-reduced-motion: reduce) {
.spinner { animation: none; border-top-color: #00FFD1; opacity: 0.7; }
}
Never rely solely on animation to indicate loading — include a visually hidden text label like 'Loading...' for screen readers. Use the CSS Loader Generator to create and customize loaders with all accessibility attributes built in.