12 CSS Progress Bar Designs 09 / 12
Circular Counter
A radial chart with a synchronised counting number in the centre.
The code
<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> <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>.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;
} .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;
}// 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);
}); // 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);
});