8pt

Progress Ring

Progress Ring•Numbers
Overview

A pure CSS progress ring: two half rings rotate to draw any angle while the number in the middle counts up. Click to set a random new target.

Plain HTML, CSS and a little vanilla JS, with no framework dependency. Copy the prompt into Claude Code, Cursor, Trae or Codex and the model adds Progress Ring to your project in your colors.

Where to use it

Data highlights, growth metrics, counters and stat cards.

Install

With Builder, install it with the 8pt CLI or call it directly from an editor connected to 8pt MCP.

$ npm i -g @8pt-dev/cli && 8pt add progress-ring
mcp › Use 8pt MCP to call get_component("progress-ring")

Code

Source for Progress Ring. Motion uses only transform and opacity and respects the Reduce motion setting.

<div class="x-ring">
  <button type="button" class="x-ring__dial" aria-label="Set new progress">
    <span class="x-ring__half x-ring__half--r"><i></i></span>
    <span class="x-ring__half x-ring__half--l"><i></i></span>
    <span class="x-ring__face"><b>0</b><small>%</small></span>
  </button>
  <div class="x-ring__meta"><b>Storage</b><span>Used <em>0</em> / 100 GB</span></div>
</div>
.x-ring{display:flex;align-items:center;gap:20px;color:var(--d-fg,#fff)}
.x-ring__dial{--sz:132px;--th:12px;position:relative;width:var(--sz);height:var(--sz);padding:0;border:0;border-radius:50%;background:var(--d-line,rgba(255,255,255,.12));cursor:pointer;flex:none}
.x-ring__half{position:absolute;top:0;width:50%;height:100%;overflow:hidden}
.x-ring__half--r{left:50%}
.x-ring__half--l{left:0}
.x-ring__half i{position:absolute;top:0;width:200%;height:100%;border-radius:50%;transform:rotate(0);transition:transform .55s linear}
.x-ring__half--r i{left:-100%;background:linear-gradient(90deg,var(--d-accent,#4D74FF) 50%,transparent 50%)}
.x-ring__half--l i{left:0;background:linear-gradient(90deg,transparent 50%,color-mix(in srgb,var(--d-accent,#4D74FF) 70%,#9ef0ff) 50%)}
.x-ring__face{position:absolute;inset:var(--th);display:flex;align-items:center;justify-content:center;border-radius:50%;background:var(--d-bg,#09090d)}
.x-ring__face b{font:700 34px/1 var(--d-font,system-ui,sans-serif);letter-spacing:-.03em;font-variant-numeric:tabular-nums}
.x-ring__face small{margin-left:2px;font:600 14px/1 var(--d-font,system-ui,sans-serif);color:var(--d-muted,#9b9ba8);transform:translateY(6px)}
.x-ring__meta{display:grid;gap:6px}
…
const init = (root) => {
  const dial = root.querySelector('.x-ring__dial'), r = dial.querySelector('.x-ring__half--r i'), l = dial.querySelector('.x-ring__half--l i');
  const num = dial.querySelector('b'), used = root.querySelector('.x-ring__meta em');
  let cur = 0, raf = 0, tl;
  const set = (p) => {
    const deg = p * 3.6, from = cur, fdeg = from * 3.6; cur = p;
    // right half sweeps 0-180deg, left half 180-360deg; when crossing 180 the halves run one after the other
    const rd = Math.min(deg, 180), ld = Math.max(deg - 180, 0);
    clearTimeout(tl); r.style.transitionDelay = l.style.transitionDelay = '0s';
    if (deg > 180 && fdeg < 180) { r.style.transform = `rotate(${rd}deg)`; l.style.transitionDelay = '.55s'; l.style.transform = `rotate(${ld}deg)`; }
    else if (deg < 180 && fdeg > 180) { l.style.transform = `rotate(${ld}deg)`; r.style.transitionDelay = '.55s'; r.style.transform = `rotate(${rd}deg)`; }
    else { r.style.transform = `rotate(${rd}deg)`; l.style.transform = `rotate(${ld}deg)`; }
…
Subscribe to Builder to see the full source.See pricing

More numbers components

View all

Number Ticker

More components

View all

AI Prompt Composer

Film Strip Carousel

Auto Tab Showcase

Media Bento Grid

Agent Chat Window

Model Pill Marquee

Director Art Cards

Mac File Window

Keep exploring