Back
A comprehensive guide to building a smooth, accessible loading indicator with customizable color variations using modern CSS techniques.
<div class="loader" role="status" aria-label="Loading">
<span class="sr-only">Loading...</span>
</div>
The role="status" attribute informs screen readers that this element communicates a status update, while aria-label="Loading" provides an explicit label that will be announced. The sr-only class contains text that will be visually hidden but remains accessible to screen readers, providing additional context.
.loader {
width: 3rem;
height: 3rem;
border-radius: 50%;
border: 0.25rem solid #6366f1;
border-color: #6366f1 transparent #6366f1 transparent;
position: relative;
}
This creates a circular element with a fixed size of 3rem (48px at default font size), which provides good visibility without dominating the interface. We use border-radius: 50% to create a perfect circle, while the strategic use of border-color creates segments of visibility—the loader appears as two arcs rather than a full circle. We use rem units to ensure the loader scales appropriately with the user's font size settings.
Next, we implement the screen-reader-only utility class following established accessibility best practices:
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
This technique visually hides content while keeping it accessible to screen readers—a critical pattern for maintaining accessibility without affecting visual design. Rather than using display: none (which would remove the content from the accessibility tree), we position it off-screen while maintaining its presence in the DOM.
Now for the animation that brings our loader to life:
@keyframes rotation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.loader {
/* previous properties */
animation: rotation 1.2s cubic-bezier(0.55, 0.15, 0.45, 0.85) infinite;
}
The @keyframes rule defines a simple 360-degree rotation. What makes this animation feel polished is the carefully selected timing function—a custom cubic-bezier(0.55, 0.15, 0.45, 0.85) curve that creates a slightly eased rotation instead of a mechanical linear movement. The 1.2-second duration strikes a balance between being too fast (which can appear frantic) and too slow (which might suggest poor performance).
Finally, we create semantic color variations to match different UI contexts:
/* Different color variations */
.loader-primary {
border: 0.25rem solid #3b82f6;
border-color: #3b82f6 transparent #3b82f6 transparent;
}
.loader-success {
border: 0.25rem solid #22c55e;
border-color: #22c55e transparent #22c55e transparent;
}
.loader-warning {
border: 0.25rem solid #f59e0b;
border-color: #f59e0b transparent #f59e0b transparent;
}
.loader-error {
border: 0.25rem solid #ef4444;
border-color: #ef4444 transparent #ef4444 transparent;
}
These modifier classes follow a consistent naming convention and use a semantic color palette adapted from popular design systems. To implement these variations, simply combine the base class with a modifier: <div class="loader loader-success" role="status" aria-label="Loading">. This approach maintains separation of concerns—structure and basic animation in the base class, with color variations as optional modifiers.
<div class="loader" role="status" aria-label="Loading">
<span class="sr-only">Loading...</span>
</div>
<style>
.loader {
width: 3rem;
height: 3rem;
border-radius: 50%;
border: 0.25rem solid #6366f1;
border-color: #6366f1 transparent #6366f1 transparent;
animation: rotation 1.2s cubic-bezier(0.55, 0.15, 0.45, 0.85) infinite;
position: relative;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
@keyframes rotation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* Different color variations */
.loader-primary {
border: 0.25rem solid #3b82f6;
border-color: #3b82f6 transparent #3b82f6 transparent;
}
.loader-success {
border: 0.25rem solid #22c55e;
border-color: #22c55e transparent #22c55e transparent;
}
.loader-warning {
border: 0.25rem solid #f59e0b;
border-color: #f59e0b transparent #f59e0b transparent;
}
.loader-error {
border: 0.25rem solid #ef4444;
border-color: #ef4444 transparent #ef4444 transparent;
}
</style>
Thank you for reading this article.
Comments
No comments yet. Be the first to comment!