webdevpuneet.com

Gradient Stat Ring — Free HTML CSS JS Progress Ring Snippet

Gradient Stat Ring · Charts · Plain HTML, CSS & JS · Live preview

CategoryCharts

Copy & paste

Source Code

<div class="gr-grid" id="grGrid">
  <div class="gr-card">
    <svg class="gr-ring" viewBox="0 0 120 120" data-value="74">
      <defs><linearGradient id="grA" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#34d399"/><stop offset="1" stop-color="#22d3ee"/></linearGradient></defs>
      <circle class="gr-track" cx="60" cy="60" r="52"/>
      <circle class="gr-fill" cx="60" cy="60" r="52" stroke="url(#grA)"/>
      <text class="gr-pct" x="60" y="66">0%</text>
    </svg>
    <div class="gr-label">Storage used</div>
  </div>
  <div class="gr-card">
    <svg class="gr-ring" viewBox="0 0 120 120" data-value="92">
      <defs><linearGradient id="grB" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#a78bfa"/><stop offset="1" stop-color="#ec4899"/></linearGradient></defs>
      <circle class="gr-track" cx="60" cy="60" r="52"/>
      <circle class="gr-fill" cx="60" cy="60" r="52" stroke="url(#grB)"/>
      <text class="gr-pct" x="60" y="66">0%</text>
    </svg>
    <div class="gr-label">Goal progress</div>
  </div>
  <div class="gr-card">
    <svg class="gr-ring" viewBox="0 0 120 120" data-value="48">
      <defs><linearGradient id="grC" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#fbbf24"/><stop offset="1" stop-color="#f97316"/></linearGradient></defs>
      <circle class="gr-track" cx="60" cy="60" r="52"/>
      <circle class="gr-fill" cx="60" cy="60" r="52" stroke="url(#grC)"/>
      <text class="gr-pct" x="60" y="66">0%</text>
    </svg>
    <div class="gr-label">CPU load</div>
  </div>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), 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

Pure SVG rings
Stroke-dash geometry, no chart library.
Gradient strokes
Per-ring linearGradient stroke fills.
Counting label
Centre number eases up to the value.
Synced timing
Arc and count finish together.
Top-start origin
SVG rotated so fill begins at 12 o'clock.
Tabular figures
Equal-width digits prevent jitter.
Scroll-triggered
IntersectionObserver fires once in view.
Clamped values
0–100 guard prevents overdraw.

About this UI Snippet

Gradient Stat Ring — Circular Progress With a Counting Label

Screenshot of the Gradient Stat Ring snippet rendered live

The gradient stat ring is the circular progress indicator you see on dashboards and fitness apps — a gradient arc that sweeps around to represent a percentage while the number in the centre counts up to match. This snippet builds a responsive grid of them with pure SVG, CSS, and a small vanilla JavaScript driver, with no charting library.

Stroke-dash geometry

Each ring is two stacked SVG circles: a muted track and a coloured fill, both with the same radius. The fill's progress is controlled by the classic stroke-dasharray / stroke-dashoffset trick. The dash array is set to the full circumference (2πr) so a single dash wraps the whole circle, and the offset is then set to the same length to hide it entirely. Animating the offset down to circumference × (1 − value/100) reveals exactly that fraction of the arc, which is how the ring "fills".

Why the SVG is rotated

By default SVG circles start their stroke at 3 o'clock, so the whole <svg> is rotated -90deg to move the start point to 12 o'clock — the natural top-of-the-clock origin people expect from a progress ring. The centre percentage <text> is then counter-rotated 90deg around the ring's centre so the label stays upright despite the parent rotation.

Gradient strokes

Each fill is painted with stroke="url(#id)" referencing an SVG <linearGradient>, giving the arc a two-colour gradient that reads as premium and distinguishes one metric from another. Because the gradient is defined per-SVG with a unique id, several rings can carry different colour ramps on the same page without clashing.

Synchronised sweep and count

The arc animation is pure CSS — a transition on stroke-dashoffset with an ease-out cubic-bezier so it decelerates as it lands. The centre number is animated separately in JavaScript with requestAnimationFrame over the same duration and an ease-out curve, so the label and the arc finish together. The figure uses font-variant-numeric: tabular-nums so it does not jitter horizontally while counting.

Scroll-triggered, once

An IntersectionObserver starts every ring when the grid is 30% in view and then disconnects, so the fill and count play exactly once as the section appears — the standard performant trigger, with no scroll listener. Values are clamped to 0–100 so out-of-range data can't overdraw the arc.

Customizing it

Set each ring's target with a data-value attribute, change the gradient stops, adjust the stroke-width for thicker or thinner arcs, or tune the transition duration. Add more cards and the grid wraps responsively. Pair the rings with a metric card grid or a glass stat card for a complete dashboard panel.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSThree stat rings render empty at 0%.
  2. 2
    Scroll them into viewEach arc sweeps to its value and the number counts up.
  3. 3
    Note the gradientsEvery ring carries its own two-colour stroke.
  4. 4
    Set a valueChange a ring's data-value attribute.
  5. 5
    Recolor a ringEdit its linearGradient stops.
  6. 6
    Adjust thicknessTune the stroke-width on track and fill.

Real-world uses

Common Use Cases

KPI row above a metric grid
Show completion or health scores as gradient rings beside a metric card grid, with the percentage counting up as the card scrolls into view.
Storage and quota usage
Put a gradient ring inside a glass stat card to show how much space or plan allowance is used, with the arc and the number finishing together.
Fitness activity rings
Pair rings for move, exercise and stand with a streak tracker, each ring carrying its own gradient from a linearGradient definition.
Goal tracking panels
Show progress toward a target next to a feature checklist, using pure SVG stroke-dash geometry instead of pulling in a charting library.
Report headline percentages
Lead a report with big percentage rings above a donut chart, triggering the sweep on scroll so readers see the animation as they arrive.
Related: Range Area Chart
See the Range Area Chart for a related charts pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

Each ring is two SVG circles — a track and a fill — with the fill controlled by stroke-dasharray and stroke-dashoffset. The dash array equals the circumference, 2 times pi times r, so one dash wraps the circle; the offset starts at the full length to hide it, then animates down to circumference times (1 minus value over 100) to reveal exactly that fraction.

By default an SVG circle's stroke begins at 3 o'clock, so the whole svg is rotated -90 degrees to move the start to 12 o'clock, the origin people expect from a progress ring. The centre percentage text is counter-rotated 90 degrees around the ring's centre so the label stays upright.

The arc uses a CSS transition on stroke-dashoffset with an ease-out cubic-bezier. The centre number is animated separately with requestAnimationFrame over the same duration and an ease-out curve, so both finish together. The figure uses tabular-nums so it does not shift horizontally while counting.

Yes. Each fill references its own SVG linearGradient by id with stroke="url(#id)", so every ring can carry a distinct two-colour ramp on the same page without clashing. Change the gradient stops to recolour a ring, and keep the ids unique per ring.

Render the SVG in your template and compute strokeDashoffset from a value prop as circumference times (1 minus value/100). Run the centre count-up in a mount effect with requestAnimationFrame, writing to a ref'd text node, and start it from an IntersectionObserver. Give each gradient a unique id (e.g. include the component key) so multiple instances do not collide.