Skip to content

Circular Progress Indicator

SVG ring progress indicators with an animated fill, count-up percentage, glow colors and a slider-driven demo ring with color thresholds.

Live preview

Open in new tab

Code

A set of circular progress indicators built with an inline SVG ring, pure CSS and a small vanilla JavaScript helper. Each ring shows a percentage in the middle and fills with a glowing stroke, which makes it a good fit for dashboards, storage meters, goal trackers, upload screens and course progress.

How it works

The root section.cpr-root holds a .cpr-grid of .cpr-item cards. Every card contains a .cpr-ring with role="progressbar", an svg.cpr-svg with two circles and a .cpr-value label. Both circles use pathLength="100", so the SVG treats the circumference as 100 units no matter how large the ring is drawn. .cpr-track is the faint background ring, and .cpr-bar uses stroke-dasharray="100" with stroke-dashoffset set to 100 - value. The SVG is rotated by -90 degrees so the bar starts at twelve o'clock, and stroke-linecap: round gives it soft ends.

On DOMContentLoaded the script reads data-value from every .cpr-ring, resets the bar to empty and then sets the real offset, so the CSS transition on stroke-dashoffset animates the fill. At the same time .cpr-num counts up from 0. The last card links a .cpr-range slider to its ring: every input event updates the stroke, the number and the ARIA values. Without JavaScript the rings still show the correct value, because the offset is written directly in the markup.

Customization

  • data-value on .cpr-ring: the percentage from 0 to 100. Also update aria-valuenow and the stroke-dashoffset attribute (100 minus the value) for the no-JavaScript fallback.
  • .cpr-item--cyan, .cpr-item--lime and .cpr-item--pink: each sets --cpr-color, the color of the bar, its glow and the percent sign. Add your own modifier for another color.
  • data-thresholds="40,75" on a ring inside .cpr-item--interactive: below the first number the bar is red, from the first number it gets .is-mid (amber), from the second .is-high (green).
  • --cpr-size on .cpr-root: the maximum diameter of the rings.
  • --cpr-thickness: the stroke width, for example 6px for a thin ring or 14px for a chunky one.
  • --cpr-speed in style.css and DURATION in script.js: the fill and count-up duration; keep both in sync.
  • grid-template-columns in .cpr-grid: two columns on phones and four from 860px.

Accessibility

Each .cpr-ring has role="progressbar", an aria-label, aria-valuemin, aria-valuemax and aria-valuenow, and the script adds aria-valuetext such as "72 percent". The SVG and the visible number are aria-hidden so screen readers do not read the value twice. The range input is wrapped in its own label and shows a clear outline on :focus-visible. With prefers-reduced-motion: reduce the bar does not animate and the number appears at its final value.

Usage notes

Several .cpr-root sections can share a page; the script handles each one separately. To update a ring from your own code, change its data-value and dispatch an input event on a linked .cpr-range, or reuse the same logic in your app.

Requirements

No dependencies. Pure HTML, CSS and JavaScript.

Circular Progress Indicator – Pure CSS progress bars component screenshot
Circular Progress Indicator · Pure CSS Progress Bars

More Progress Bars

Animated Progress Bars – Bootstrap 5 progress bars preview

Animated Progress Bars

Bootstrap 5 dark dashboard with labelled bars that fill on scroll, a striped animated build progress bar and a stacked storage bar.