webdevpuneet.com

Concentric Rings Progress Loader — Multi-Stage SVG Loading Indicator

Concentric Rings Progress Loader · Loaders · Plain HTML, CSS & JS · Live preview

CategoryLoaders

Copy & paste

Source Code

<div class="demo">
  <div class="rings-wrap">
    <svg class="rings-svg" viewBox="0 0 120 120" role="img" aria-label="Loading three stages: assets, config, data">
      <circle class="ring-track" cx="60" cy="60" r="52" />
      <circle class="ring-track" cx="60" cy="60" r="38" />
      <circle class="ring-track" cx="60" cy="60" r="24" />
      <circle class="ring-fill ring-1" cx="60" cy="60" r="52" />
      <circle class="ring-fill ring-2" cx="60" cy="60" r="38" />
      <circle class="ring-fill ring-3" cx="60" cy="60" r="24" />
    </svg>
    <div class="rings-center">
      <span class="rings-pct" id="pct">0%</span>
      <span class="rings-label" id="label">Loading assets…</span>
    </div>
  </div>
  <div class="rings-legend">
    <span><i style="background:#6366f1"></i>Assets</span>
    <span><i style="background:#22d3ee"></i>Config</span>
    <span><i style="background:#34d399"></i>Data</span>
  </div>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Three independent SVG progress rings, each representing one distinct loading stage
Rings fill sequentially — only one active at a time, while completed rings stay visually full
Live overall percentage in the center derived mathematically from stage index plus in-stage progress
Dynamic center label updates to name whichever stage is currently active
Standard stroke-dasharray/stroke-dashoffset SVG technique, computed once per ring radius
requestAnimationFrame-driven animation loop instead of setInterval, for smoother frame timing
role="img" with a descriptive aria-label summarizing all three stages for screen readers
Small color-coded legend beneath the rings mapping each ring to its stage name

About this UI Snippet

Concentric Rings Progress Loader — One Ring per Loading Stage

Screenshot of the Concentric Rings Progress Loader snippet rendered live

A single spinner or progress bar can only communicate "something is happening" and, at best, one overall percentage. This loader instead uses three nested SVG rings, filled one at a time in sequence, so a genuinely multi-stage loading process — asset loading, then config, then data — has a visual home for each individual stage's progress, not just a single blended number.

The stroke-dasharray / stroke-dashoffset technique

Each ring is an SVG <circle> with no fill, just a stroke, and a stroke-dasharray set to that circle's exact circumference (2 × π × r, precomputed per radius — 326.7 for r=52, 238.8 for r=38, 150.8 for r=24). Because the dash pattern is exactly one segment as long as the whole circle, the circle appears either fully drawn or, when stroke-dashoffset equals the same value, entirely hidden. Animating stroke-dashoffset from that full value down to 0 makes the ring appear to fill in continuously, clockwise from its start point — a standard SVG progress-ring trick, applied here three times independently.

Why the SVG is rotated -90 degrees

SVG circles start their path at the 3 o'clock position by default. Rotating the whole <svg> element -90deg moves that starting point to 12 o'clock, which is the conventional "start" position for a progress indicator — the rotation is applied once to the parent SVG rather than adjusting each circle's path math individually.

Sequencing three independent progress values into one loop

A single requestAnimationFrame loop (tick) advances one shared stageProgress value for whichever ring is currently active (stages[stageIndex]). Once that stage reaches 1 (fully filled), the loop increments stageIndex, resets stageProgress to 0, and continues animating the *next* ring — so only one ring is ever actively filling at a time, while previously completed rings simply stay full. The center percentage (overall) is derived from both values together: ((stageIndex + stageProgress) / stages.length) * 100, so it advances smoothly across the whole sequence rather than jumping in three big steps.

Where a multi-stage loader beats a single progress bar

Multi-step processes — app cold-start sequences, multi-phase file uploads, onboarding data imports — genuinely have distinct phases with their own completion criteria. Showing them as one blended percentage hides *which* phase is currently slow; three separate, individually-labeled rings that fill one after another give a user (or a developer debugging a slow load) a much clearer read on exactly what's happening and roughly how far along it is.

Step by step

How to Use

  1. 1
    Adjust the ring radii and dasharray values togetherIf you change an r attribute on a circle, recompute its stroke-dasharray as 2 × π × r and update both the SVG and matching JS dasharray values.
  2. 2
    Edit the stages arrayEach stage needs a ring element reference, its matching dasharray number, and a label string shown while it is active.
  3. 3
    Tune the fill speedChange the 0.045 increment added to stageProgress each frame — a smaller number fills more slowly, a larger one more quickly.
  4. 4
    Replace the simulated progress with real progressInstead of incrementing stageProgress by a fixed amount per frame, set it directly from real load-event progress (e.g. a fetch's reported bytes loaded).
  5. 5
    Swap the ring colorsUpdate the stroke colors on .ring-1/.ring-2/.ring-3 and the matching legend dot colors in the HTML panel to fit your palette.

Real-world uses

Common Use Cases

Multi-Phase App Cold Start
Show distinct progress for asset loading, configuration, and initial data fetch during an app's startup sequence.
UPLOAD
Multi-Step File Processing
Represent upload, validation, and processing as three separate stages a user can track individually.
ONBOARD
Data Import Wizards
Visualize the distinct steps of an import pipeline — parsing, validating, and writing — as it runs.
SETUP
First-Run Setup Screens
Give a new user a sense of exactly what's happening during a multi-step first-time app setup.
Related: Multi-File Upload Queue
See the Multi-File Upload Queue for a related loaders pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

Setting stroke-dasharray to the circle's exact circumference creates one dash segment as long as the whole circle, so the ring renders as fully drawn. Animating stroke-dashoffset toward 0 progressively reveals more of that segment from the start point, visually filling the ring.

SVG circles begin their path at the 3 o'clock position by default. Rotating the whole SVG element -90 degrees moves the visual starting point to 12 o'clock, matching the conventional orientation for a circular progress indicator.

Yes — remove the stageIndex advancement logic and instead update all three rings' stroke-dashoffset independently based on three separate real progress values reported concurrently, rather than gating them one after another.

Replace the fixed 0.045-per-frame increment in tick() with an assignment driven by real progress events — for example a fetch response's reported bytes loaded divided by total bytes for that stage.

The tick() function detects stageIndex has advanced past the last stage, sets the label to "Ready" and the percentage to a fixed 100%, and returns without scheduling another animation frame, stopping the loop cleanly.

The SVG has role="img" with a descriptive aria-label summarizing all three stages up front. For live progress updates specifically, consider adding aria-live="polite" to the percentage or label element so changes are announced as loading proceeds.