Back

Creating an Accessible, Animated Loading Spinner

A comprehensive guide to building a smooth, accessible loading indicator with customizable color variations using modern CSS techniques.

HTML

We'll create a loading spinner that incorporates proper accessibility attributes to ensure a good experience for all users, including those using assistive technologies.
        <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.

CSS

Let's start by styling our base loader component:
        .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.

Whole code

<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!

More loaders

Similar

See also