Progress Ring
Gradient SVG ring that fills up to its value when it appears, then glides to every new value: a real progress indicator.
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



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"witharia-valuemin,aria-valuemax,aria-valuenowand an up-to-datearia-valuetext. Give it a precise name witharia-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-liveregion.
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.
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):
<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 / property | Default | Effect |
|---|---|---|
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
aria-valuenow at each step (file uploaded, lesson finished, field filled). The script watches that attribute: no function to call, no global variable..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.