Back to CSS Progress Bars Circular Counter Light JS
Share
HTML
<div
  class="pb-count"
  role="progressbar"
  aria-valuenow="0"
  aria-valuemin="0"
  aria-valuemax="100"
  aria-label="Score"
  data-pb-count="86"
>
  <svg class="pb-count-ring" viewBox="0 0 100 100" aria-hidden="true">
    <defs>
      <linearGradient id="pb-count-grad" x1="0" y1="0" x2="1" y2="1">
        <stop offset="0%" stop-color="#34d399" />
        <stop offset="100%" stop-color="#10b981" />
      </linearGradient>
    </defs>
    <circle class="pb-count-bg" cx="50" cy="50" r="42"></circle>
    <circle class="pb-count-prog" cx="50" cy="50" r="42" pathLength="100"></circle>
  </svg>
  <div class="pb-count-meta">
    <strong data-pb-count-num>0</strong>
    <span>score</span>
  </div>
</div>
CSS
.pb-count {
  position: relative;
  width: 130px;
  height: 130px;
  font-family: system-ui, sans-serif;
}

.pb-count-ring {
  width: 100%;
  height: 100%;
  transform: rotate(-90deg);
}

.pb-count-bg,
.pb-count-prog {
  fill: none;
  stroke-width: 7;
  stroke-linecap: round;
}

.pb-count-bg {
  stroke: rgba(255, 255, 255, 0.06);
}

.pb-count-prog {
  stroke: url(#pb-count-grad);
  stroke-dasharray: 100;
  stroke-dashoffset: 100;
  transition: stroke-dashoffset 1.4s cubic-bezier(0.5, 0, 0.3, 1.2);
}

.pb-count.is-ready .pb-count-prog {
  stroke-dashoffset: calc(100 - (var(--pb-count-pct, 0) * 100));
}

.pb-count-meta {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  line-height: 1;
}

.pb-count-meta strong {
  display: block;
  font-size: 28px;
  font-weight: 700;
  color: #f0eeff;
  font-variant-numeric: tabular-nums;
  line-height: 1;
  letter-spacing: -0.02em;
}

.pb-count-meta span {
  display: block;
  font-size: 9.5px;
  font-weight: 600;
  color: #34d399;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  line-height: 1;
}
JS
// Reads the target value from data-pb-count and animates the
// SVG ring + centre number from 0 to that value over 1.4s.
// Drop this on every page where you render a .pb-count element.
document.querySelectorAll("[data-pb-count]").forEach(function (el) {
  var target = Number(el.dataset.pbCount) || 0;
  var num = el.querySelector("[data-pb-count-num]");

  // Drive the ring's stroke-dashoffset via a CSS custom property
  el.style.setProperty("--pb-count-pct", String(target / 100));
  requestAnimationFrame(function () {
    el.classList.add("is-ready");
  });

  // Tick the centre number in sync with the 1.4s ring transition
  var start = null;
  var duration = 1400;
  function tick(t) {
    if (start === null) start = t;
    var p = Math.min(1, (t - start) / duration);
    var eased = 1 - Math.pow(1 - p, 3); // ease-out cubic
    var v = Math.round(target * eased);
    if (num) num.textContent = v;
    el.setAttribute("aria-valuenow", String(v));
    if (p < 1) requestAnimationFrame(tick);
  }
  requestAnimationFrame(tick);
});