Data✨ Premium

Progress Ring

Gradient SVG ring that fills up to its value when it appears, then glides to every new value: a real progress indicator.

SVGCSS

Updated

This effect is part of Effect.Labs — 840 vanilla effects, some free, some premium. Data has 28 effects, including 1 free. Explore the category →

3 usage examples

Volunteer profile 75% complete — the ring circles the photo — Progress Ring example 1

① Volunteer profile 75% complete — the ring circles the photo

WhenIn her member area, the volunteer sees her avatar circled by a ring three quarters full. She ticks "Your availability" then "Driving licence": the ring moves to 85% then closes at 100%.
WhyA ring around the photo says "your profile" without another word, and watching it grow with each piece of information makes people want to finish — charities need those details to offer missions.
Settings--prng-size:112px, --prng-stroke:7, --prng-from:#14b8a6, --prng-to:#22c55e, --prng-track:#e2ece7, transparent background. The centre text is hidden by the scene CSS to make room for the avatar; the "Profile … complete" pill repeats aria-valuetext. Each ticked box adds its points to aria-valuenow.
Progress dots at the end of each lesson — programme of a music theory course — Progress Ring example 2

② Progress dots at the end of each lesson — programme of a music theory course

WhenThe course page lists six lessons with their length; at the end of each row, a small 34 px ring shows where the learner stands: full for finished lessons, partly filled for those in progress, empty for the rest. They fill when the list reaches the screen.
WhyIn a list, a ring reads faster than a written percentage: the eye spots at once where to resume. At this size, the appearing motion stays discreet.
SettingsSix roots, one per row: --prng-size:34px, --prng-stroke:12 (the stroke keeps its weight at small size), gradient #f59e0b→#b45309, track #efe5d6, centre text hidden. aria-label="Lesson 3 progress" on every ring.
Match rate after a career quiz — the large ring of the result — Progress Ring example 3

③ Match rate after a career quiz — the large ring of the result

WhenAfter the twelfth question, the student clicks "See my result": a large ring fills up to 75% next to the suggested trade. "Next trade" makes the ring glide to the next trade's value.
WhyThe result is the awaited moment of the quiz: a ring filling as it opens holds attention for a second and gives the number weight; it then stays a steady reference when comparing trades.
Settings--prng-size:168px, --prng-stroke:9, gradient #818cf8→#f472b6, track rgba(129,140,248,.18), transparent background. The result panel is hidden until the click, the ring starts as it appears; "Next trade" writes 62 then 54 into aria-valuenow.

How it works

The value lives in one place: aria-valuenow on .prng-ring (0 to 100 by default, or between aria-valuemin and aria-valuemax). The script works out the share to paint, writes the centre text ("75%" in English, "75 %" in French, from the page's lang attribute) and copies it into aria-valuetext. The number shown and the ring can no longer disagree.

The ring is an SVG circle of radius 50 whose stroke is 314.16 units long (its circumference). While it is off screen it stays empty; once half visible (IntersectionObserver), the script sets --prng-offset and the 2 s CSS transition (ease-out) fills it from the top. This only happens once.

After that, every new value written to aria-valuenow (by your code, after a user action) is followed: a MutationObserver updates the text and the ring glides from the old value to the new one. Each ring gets its own gradient id, so several rings in one page, even a hidden one, keep their colours; nothing is added to window.

Accessibility

  • Role: role="progressbar" with aria-valuemin, aria-valuemax, aria-valuenow and an up-to-date aria-valuetext. Give it a precise name with aria-label ("Profile completed", "Lesson 3"): without it, users only hear "Progress, 75%".
  • Drawing hidden: the SVG carries aria-hidden="true"; screen readers announce the value, not the circles.
  • Reduced motion: under prefers-reduced-motion: reduce, the transition is off and the ring shows its value as soon as it appears; new values also apply without gliding.
  • Is a change announced? A progressbar role is not a live region: a new value is read when the user comes back to the ring. To announce it at once, add your own message in an aria-live region.

Browser compatibility

SVG, CSS variables and dependency-free JavaScript (about 2 KB). Measured on 08/11/2026 in Chromium 151, Firefox 153 and WebKit 26.5 (Playwright): fill on appearance, a ring further down waiting for the scroll, a new value on the 2nd copy without touching the 1st, 390 px, reduced motion, no error. Versions taken from @mdn/browser-compat-data: IntersectionObserver sets the minimum.

Chrome 51+✓ Full (measured)
Firefox 55+✓ Full (measured)
Safari 12.1+✓ Full (measured in WebKit)
Edge 79+✓ Full (Chromium engine)
iOS Safari 12.2+✓ Not checked on a real device
Android Chrome 51+✓ Full (Chromium engine)

Without IntersectionObserver, the ring takes its value on load. Without JavaScript, the ring stays empty but the HTML text ("75%") remains readable.

The code

HTML structure to paste into your page (the CSS + JS come with your purchase):

index.html — structure
<div class="fx-0224-root">
  <div class="prng-ring" role="progressbar" aria-label="Progress" aria-valuemin="0" aria-valuemax="100" aria-valuenow="75">
    <svg class="prng-svg" viewBox="0 0 120 120" aria-hidden="true">
      <defs><linearGradient id="prng-grad">…</linearGradient></defs>
      <circle class="prng-track"></circle>
      <circle class="prng-fill"></circle>
    </svg>
    <span class="prng-text">75%</span>
  </div>
</div>

One-time payment, no subscription — the code is yours forever

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
Value (HTML — aria-valuenow on .prng-ring) 75 Change the attribute, at start or later in JavaScript (ring.setAttribute('aria-valuenow', 90)): text and ring follow.
Bounds (HTML — aria-valuemin and aria-valuemax) 0 and 100 For 3 lessons out of 8: aria-valuemin="0" aria-valuemax="8" aria-valuenow="3"; the centre then shows 38%.
Name for screen readers (HTML — aria-label) Progress Say what progresses: "Profile completed", "Fundraising goal".
Colours (CSS — --prng-from, --prng-to, --prng-track) #6366f1 → #d946ef · indigo at 20% The two shades of the gradient and the colour of the empty part.
Size and thickness (CSS — --prng-size, --prng-stroke) 120px · 8 The centre text is 20% of the size. Thickness is in drawing units (120 wide): raise it below 50 px so the ring stays visible.
Duration (CSS — --prng-duration) 2s Length of the fill and of later glides.
Background, text and height (CSS — --prng-bg, --prng-text, --prng-height) #0a0a0f · #fff · 300px On a light page: --prng-bg:transparent; --prng-text:#0f172a; --prng-height:0px.

FAQ

Write the new value into aria-valuenow at each step (file uploaded, lesson finished, field filled). The script watches that attribute: no function to call, no global variable.
Yes: hide .prng-text with CSS and place your element over the ring (absolute position on a shared parent), like the avatar in the example. The value is still announced through aria-valuetext.
It waits until it is half visible before filling. At the very top, it fills as soon as the page opens; further down, it does so when the reader scrolls to it.