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!