Open demo

CSS code

HTML
<div class="pg"><span class="lbl">Radial progress rings</span>
<div class="pg-rings">
  <article class="pg-card">
    <div class="pg-ring" style="--p:72" role="progressbar" aria-labelledby="pg-r1" aria-valuemin="0" aria-valuemax="100" aria-valuenow="72" aria-valuetext="72%, $360,000 of $500,000">
      <svg viewBox="0 0 100 100" aria-hidden="true"><circle class="trk" cx="50" cy="50" r="42"/><circle class="bar" cx="50" cy="50" r="42" pathLength="100"/></svg><span aria-hidden="true">72%</span></div>
    <div class="pg-ring-text"><h3 class="pg-title" id="pg-r1">Q4 sales target</h3><p class="pg-sub pg-num">$360,000 of $500,000</p>
      <span class="pg-status is-good"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>On track</span></div>
  </article>
  <article class="pg-card">
    <div class="pg-ring is-warn" style="--p:86" role="progressbar" aria-labelledby="pg-r2" aria-valuemin="0" aria-valuemax="100" aria-valuenow="86" aria-valuetext="86%, 43 GB of 50 GB">
      <svg viewBox="0 0 100 100" aria-hidden="true"><circle class="trk" cx="50" cy="50" r="42"/><circle class="bar" cx="50" cy="50" r="42" pathLength="100"/></svg><span aria-hidden="true">86%</span></div>
    <div class="pg-ring-text"><h3 class="pg-title" id="pg-r2">Storage used</h3><p class="pg-sub pg-num">43 GB of 50 GB</p>
      <span class="pg-status is-warn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 9v4M12 17h.01"/><path d="M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/></svg>Near limit</span></div>
  </article>
  <article class="pg-card">
    <div class="pg-ring" style="--p:91" role="progressbar" aria-labelledby="pg-r3" aria-valuemin="0" aria-valuemax="100" aria-valuenow="91" aria-valuetext="91%, 1,183 of 1,300 accounts">
      <svg viewBox="0 0 100 100" aria-hidden="true"><circle class="trk" cx="50" cy="50" r="42"/><circle class="bar" cx="50" cy="50" r="42" pathLength="100"/></svg><span aria-hidden="true">91%</span></div>
    <div class="pg-ring-text"><h3 class="pg-title" id="pg-r3">Onboarding done</h3><p class="pg-sub pg-num">1,183 of 1,300 accounts</p>
      <span class="pg-status is-good"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>On track</span></div>
  </article>
</div></div>

<div class="pg"><span class="lbl">Target bars with a pace marker</span>
<article class="pg-card pg-targets">
  <div class="pg-targets-head"><div><h3 class="pg-title">October targets</h3><p class="pg-sub">Day 16 of 31 · updated 09:40</p></div>
    <p class="pg-key"><i aria-hidden="true"></i>Marker: where you should be today (52%)</p></div>
  <ul class="pg-list">
    <li><div class="pg-row-top"><b id="pg-t1">New MRR</b><span class="pg-num"><strong>$9,820</strong> of $15,000</span><span class="pg-status is-good"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5M5 12l7-7 7 7"/></svg>Ahead of pace</span></div>
      <div class="pg-bar" style="--p:65%;--pace:52%" role="progressbar" aria-labelledby="pg-t1" aria-valuemin="0" aria-valuemax="15000" aria-valuenow="9820" aria-valuetext="$9,820 of $15,000, 65%, ahead of pace"><i></i></div></li>
    <li><div class="pg-row-top"><b id="pg-t2">Enterprise demos booked</b><span class="pg-num"><strong>21</strong> of 40</span><span class="pg-status is-good"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>On pace</span></div>
      <div class="pg-bar" style="--p:52.5%;--pace:52%" role="progressbar" aria-labelledby="pg-t2" aria-valuemin="0" aria-valuemax="40" aria-valuenow="21" aria-valuetext="21 of 40, 53%, on pace"><i></i></div></li>
    <li><div class="pg-row-top"><b id="pg-t3">Newsletter signups</b><span class="pg-num"><strong>380</strong> of 1,000</span><span class="pg-status is-bad"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14M19 12l-7 7-7-7"/></svg>Behind pace</span></div>
      <div class="pg-bar" style="--p:38%;--pace:52%" role="progressbar" aria-labelledby="pg-t3" aria-valuemin="0" aria-valuemax="1000" aria-valuenow="380" aria-valuetext="380 of 1,000, 38%, behind pace"><i></i></div></li>
    <li><div class="pg-row-top"><b id="pg-t4">Partner referrals</b><span class="pg-num"><strong>14</strong> of 25</span><span class="pg-status is-good"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5M5 12l7-7 7 7"/></svg>Ahead of pace</span></div>
      <div class="pg-bar" style="--p:56%;--pace:52%" role="progressbar" aria-labelledby="pg-t4" aria-valuemin="0" aria-valuemax="25" aria-valuenow="14" aria-valuetext="14 of 25, 56%, ahead of pace"><i></i></div></li>
  </ul>
</article></div>

<div class="pg"><span class="lbl">Goal with milestones</span>
<article class="pg-card pg-goal">
  <div class="pg-goal-top"><div><h3 class="pg-title" id="pg-goal">2026 signup goal</h3><p class="pg-big pg-num">8,412 <small>of 10,000 signups</small></p></div>
    <span class="pg-status is-good"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5M5 12l7-7 7 7"/></svg>494 ahead of plan</span></div>
  <div class="pg-meter"><progress max="10000" value="8412" aria-labelledby="pg-goal" aria-valuetext="8,412 of 10,000 signups, 84%">84%</progress><span class="pg-ticks" aria-hidden="true"></span></div>
  <ol class="pg-miles" aria-label="Milestones">
    <li class="is-done"><strong><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>2,500</strong>Reached Mar 14</li>
    <li class="is-done"><strong><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>5,000</strong>Reached Jul 2</li>
    <li class="is-done"><strong><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>7,500</strong>Reached Sep 21</li>
    <li><strong>10,000</strong>1,588 to go</li>
  </ol>
</article></div>
CSS
/* Goal and progress widgets. Every bar or ring is a progressbar (ARIA role or native <progress>) with a name
   and a value text, and the same numbers are printed beside it. Status is a word plus an icon, not just a colour. */
.pg{--pg-ink:#0f172a;--pg-muted:#4b5768;--pg-line:#e2e8f0;--pg-track:#e2e8f0;--pg-fill:#2a78d6;--pg-warn:#b45309;--pg-warn-bg:#fef3c7;
  --pg-good:#15803d;--pg-good-bg:#dcfce7;--pg-bad:#b91c1c;--pg-bad-bg:#fee2e2;--pg-mark:#0f172a;color:var(--pg-ink)}
.pg-card{background:#fff;border:1px solid var(--pg-line);border-radius:16px;padding:20px;box-shadow:0 1px 2px rgba(15,23,42,.04),0 8px 24px rgba(15,23,42,.05);min-width:0}
.pg-title{font-size:1rem;font-weight:700}
.pg-sub{font-size:.8rem;color:var(--pg-muted)}
.pg-num{font-variant-numeric:tabular-nums}
.pg-status{display:inline-flex;align-items:center;gap:4px;padding:2px 8px 2px 6px;border-radius:999px;font-size:.74rem;font-weight:700;white-space:nowrap}
.pg-status svg{width:13px;height:13px;flex:none}
.pg-status.is-good{color:var(--pg-good);background:var(--pg-good-bg)}
.pg-status.is-warn{color:var(--pg-warn);background:var(--pg-warn-bg)}
.pg-status.is-bad{color:var(--pg-bad);background:var(--pg-bad-bg)}
/* radial rings */
.pg-rings{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px}
.pg-rings .pg-card{display:flex;align-items:center;gap:16px}
.pg-ring{position:relative;flex:none;width:96px;height:96px}
.pg-ring svg{width:100%;height:100%;transform:rotate(-90deg)}
.pg-ring circle{fill:none;stroke-width:10}
.pg-ring .trk{stroke:var(--pg-track)}
.pg-ring .bar{stroke:var(--pg-fill);stroke-linecap:round;stroke-dasharray:var(--p) 100;transition:stroke-dasharray .6s ease}
.pg-ring.is-warn .bar{stroke:#d97706}
.pg-ring span{position:absolute;inset:0;display:grid;place-items:center;font-size:1.15rem;font-weight:750;font-variant-numeric:tabular-nums}
.pg-ring-text{display:flex;flex-direction:column;gap:4px;align-items:flex-start;min-width:0}
/* target bars */
.pg-targets{display:flex;flex-direction:column;gap:18px}
.pg-targets-head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;flex-wrap:wrap}
.pg-list{list-style:none;display:flex;flex-direction:column;gap:18px}
.pg-row-top{display:flex;justify-content:space-between;align-items:baseline;gap:10px;flex-wrap:wrap;margin-bottom:8px;font-size:.88rem}
.pg-row-top b{font-weight:600;margin-right:auto}
.pg-row-top .pg-num{color:var(--pg-muted)}
.pg-row-top .pg-num strong{color:var(--pg-ink)}
.pg-bar{position:relative;height:10px;border-radius:999px;background:var(--pg-track)}
.pg-bar i{position:absolute;inset:0 auto 0 0;width:var(--p);border-radius:inherit;background:var(--pg-fill);transition:width .6s ease}
.pg-bar::after{content:"";position:absolute;left:var(--pace);top:-4px;bottom:-4px;width:2px;border-radius:2px;background:var(--pg-mark)}
.pg-key{display:flex;align-items:center;gap:8px;font-size:.78rem;color:var(--pg-muted)}
.pg-key i{width:2px;height:14px;background:var(--pg-mark);border-radius:2px}
/* goal with milestones (native <progress>) */
.pg-goal{display:flex;flex-direction:column;gap:14px}
.pg-goal-top{display:flex;justify-content:space-between;align-items:flex-end;gap:12px;flex-wrap:wrap}
.pg-big{font-size:2.2rem;font-weight:750;letter-spacing:-.02em;line-height:1.1}
.pg-big small{font-size:1rem;font-weight:600;color:var(--pg-muted);letter-spacing:0}
.pg-meter{position:relative}
.pg-meter progress{-webkit-appearance:none;appearance:none;display:block;width:100%;height:14px;border:0;border-radius:999px;background:var(--pg-track);overflow:hidden;color:var(--pg-fill)}
.pg-meter progress::-webkit-progress-bar{background:var(--pg-track);border-radius:999px}
.pg-meter progress::-webkit-progress-value{background:var(--pg-fill);border-radius:999px}
.pg-meter progress::-moz-progress-bar{background:var(--pg-fill);border-radius:999px}
.pg-ticks{position:absolute;inset:0;pointer-events:none;background:linear-gradient(90deg,transparent calc(25% - 1px),#fff calc(25% - 1px) calc(25% + 1px),transparent 0 calc(50% - 1px),#fff 0 calc(50% + 1px),transparent 0 calc(75% - 1px),#fff 0 calc(75% + 1px),transparent 0)}
.pg-miles{list-style:none;display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px}
.pg-miles li{display:flex;flex-direction:column;align-items:flex-end;text-align:right;gap:2px;font-size:.8rem;color:var(--pg-muted);padding-top:10px;border-top:2px solid var(--pg-line)}
.pg-miles li.is-done{border-top-color:var(--pg-fill)}
.pg-miles strong{display:flex;align-items:center;gap:5px;font-size:.95rem;color:var(--pg-ink);font-variant-numeric:tabular-nums}
.pg-miles svg{width:15px;height:15px;color:var(--pg-good)}
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
@media (max-width:560px){.pg-miles{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media (prefers-reduced-motion:reduce){.pg-ring .bar,.pg-bar i{transition:none}}

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div class="pg"><span class="lbl">Radial progress rings</span>
<div class="row row-cols-1 row-cols-md-3 g-3">
  <div class="col"><article class="card h-100"><div class="card-body d-flex align-items-center gap-3">
    <div class="pg-ring" style="--p:72" role="progressbar" aria-labelledby="pg-r1" aria-valuemin="0" aria-valuemax="100" aria-valuenow="72" aria-valuetext="72%, $360,000 of $500,000">
      <svg viewBox="0 0 100 100" aria-hidden="true"><circle class="trk" cx="50" cy="50" r="42"/><circle class="bar" cx="50" cy="50" r="42" pathLength="100"/></svg><span aria-hidden="true">72%</span></div>
    <div class="d-flex flex-column align-items-start gap-1 min-w-0"><h3 class="pg-title mb-0" id="pg-r1">Q4 sales target</h3><p class="pg-sub pg-num mb-0">$360,000 of $500,000</p>
      <span class="badge rounded-pill bg-success-subtle text-success-emphasis"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>On track</span></div>
  </div></article></div>
  <div class="col"><article class="card h-100"><div class="card-body d-flex align-items-center gap-3">
    <div class="pg-ring is-warn" style="--p:86" role="progressbar" aria-labelledby="pg-r2" aria-valuemin="0" aria-valuemax="100" aria-valuenow="86" aria-valuetext="86%, 43 GB of 50 GB">
      <svg viewBox="0 0 100 100" aria-hidden="true"><circle class="trk" cx="50" cy="50" r="42"/><circle class="bar" cx="50" cy="50" r="42" pathLength="100"/></svg><span aria-hidden="true">86%</span></div>
    <div class="d-flex flex-column align-items-start gap-1 min-w-0"><h3 class="pg-title mb-0" id="pg-r2">Storage used</h3><p class="pg-sub pg-num mb-0">43 GB of 50 GB</p>
      <span class="badge rounded-pill bg-warning-subtle text-warning-emphasis"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 9v4M12 17h.01"/><path d="M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/></svg>Near limit</span></div>
  </div></article></div>
  <div class="col"><article class="card h-100"><div class="card-body d-flex align-items-center gap-3">
    <div class="pg-ring" style="--p:91" role="progressbar" aria-labelledby="pg-r3" aria-valuemin="0" aria-valuemax="100" aria-valuenow="91" aria-valuetext="91%, 1,183 of 1,300 accounts">
      <svg viewBox="0 0 100 100" aria-hidden="true"><circle class="trk" cx="50" cy="50" r="42"/><circle class="bar" cx="50" cy="50" r="42" pathLength="100"/></svg><span aria-hidden="true">91%</span></div>
    <div class="d-flex flex-column align-items-start gap-1 min-w-0"><h3 class="pg-title mb-0" id="pg-r3">Onboarding done</h3><p class="pg-sub pg-num mb-0">1,183 of 1,300 accounts</p>
      <span class="badge rounded-pill bg-success-subtle text-success-emphasis"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>On track</span></div>
  </div></article></div>
</div></div>

<div class="pg"><span class="lbl">Target bars with a pace marker</span>
<article class="card"><div class="card-body d-flex flex-column gap-3">
  <div class="d-flex justify-content-between align-items-start flex-wrap gap-3"><div><h3 class="pg-title mb-0">October targets</h3><p class="pg-sub mb-0">Day 16 of 31 · updated 09:40</p></div>
    <p class="pg-key d-flex align-items-center gap-2 mb-0"><i aria-hidden="true"></i>Marker: where you should be today (52%)</p></div>
  <ul class="list-unstyled d-flex flex-column gap-3 mb-0">
    <li><div class="d-flex justify-content-between align-items-baseline flex-wrap gap-2 mb-2 pg-row-top"><b id="pg-t1">New MRR</b><span class="pg-num text-body-secondary"><strong class="text-body">$9,820</strong> of $15,000</span><span class="badge rounded-pill bg-success-subtle text-success-emphasis"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5M5 12l7-7 7 7"/></svg>Ahead of pace</span></div>
      <div class="pg-pace" style="--pace:52%"><div class="progress" role="progressbar" aria-labelledby="pg-t1" aria-valuemin="0" aria-valuemax="15000" aria-valuenow="9820" aria-valuetext="$9,820 of $15,000, 65%, ahead of pace"><div class="progress-bar" style="width:65%"></div></div></div></li>
    <li><div class="d-flex justify-content-between align-items-baseline flex-wrap gap-2 mb-2 pg-row-top"><b id="pg-t2">Enterprise demos booked</b><span class="pg-num text-body-secondary"><strong class="text-body">21</strong> of 40</span><span class="badge rounded-pill bg-success-subtle text-success-emphasis"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>On pace</span></div>
      <div class="pg-pace" style="--pace:52%"><div class="progress" role="progressbar" aria-labelledby="pg-t2" aria-valuemin="0" aria-valuemax="40" aria-valuenow="21" aria-valuetext="21 of 40, 53%, on pace"><div class="progress-bar" style="width:52.5%"></div></div></div></li>
    <li><div class="d-flex justify-content-between align-items-baseline flex-wrap gap-2 mb-2 pg-row-top"><b id="pg-t3">Newsletter signups</b><span class="pg-num text-body-secondary"><strong class="text-body">380</strong> of 1,000</span><span class="badge rounded-pill bg-danger-subtle text-danger-emphasis"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14M19 12l-7 7-7-7"/></svg>Behind pace</span></div>
      <div class="pg-pace" style="--pace:52%"><div class="progress" role="progressbar" aria-labelledby="pg-t3" aria-valuemin="0" aria-valuemax="1000" aria-valuenow="380" aria-valuetext="380 of 1,000, 38%, behind pace"><div class="progress-bar" style="width:38%"></div></div></div></li>
    <li><div class="d-flex justify-content-between align-items-baseline flex-wrap gap-2 mb-2 pg-row-top"><b id="pg-t4">Partner referrals</b><span class="pg-num text-body-secondary"><strong class="text-body">14</strong> of 25</span><span class="badge rounded-pill bg-success-subtle text-success-emphasis"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5M5 12l7-7 7 7"/></svg>Ahead of pace</span></div>
      <div class="pg-pace" style="--pace:52%"><div class="progress" role="progressbar" aria-labelledby="pg-t4" aria-valuemin="0" aria-valuemax="25" aria-valuenow="14" aria-valuetext="14 of 25, 56%, ahead of pace"><div class="progress-bar" style="width:56%"></div></div></div></li>
  </ul>
</div></article></div>

<div class="pg"><span class="lbl">Goal with milestones</span>
<article class="card pg-goal"><div class="card-body d-flex flex-column gap-3">
  <div class="d-flex justify-content-between align-items-end flex-wrap gap-3"><div><h3 class="pg-title mb-0" id="pg-goal">2026 signup goal</h3><p class="pg-big pg-num mb-0">8,412 <small>of 10,000 signups</small></p></div>
    <span class="badge rounded-pill bg-success-subtle text-success-emphasis"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5M5 12l7-7 7 7"/></svg>494 ahead of plan</span></div>
  <div class="progress" role="progressbar" aria-labelledby="pg-goal" aria-valuemin="0" aria-valuemax="10000" aria-valuenow="8412" aria-valuetext="8,412 of 10,000 signups, 84%"><div class="progress-bar" style="width:84.12%"></div><span class="pg-ticks" aria-hidden="true"></span></div>
  <ol class="pg-miles row row-cols-2 row-cols-sm-4 g-2 list-unstyled mb-0" aria-label="Milestones">
    <li class="col is-done"><div><strong><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>2,500</strong>Reached Mar 14</div></li>
    <li class="col is-done"><div><strong><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>5,000</strong>Reached Jul 2</div></li>
    <li class="col is-done"><div><strong><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>7,500</strong>Reached Sep 21</div></li>
    <li class="col"><div><strong>10,000</strong>1,588 to go</div></li>
  </ol>
</div></article></div>
CSS
/* Bootstrap .card, .progress (role="progressbar") and .badge; the rings are a small SVG. Colours via Bootstrap variables on .pg. */
.pg{--bs-body-color:#0f172a;--bs-secondary-color:#4b5768;--bs-card-border-color:#e2e8f0;
  --bs-success-text-emphasis:#15803d;--bs-success-bg-subtle:#dcfce7;--bs-danger-text-emphasis:#b91c1c;--bs-danger-bg-subtle:#fee2e2;--bs-warning-text-emphasis:#b45309;--bs-warning-bg-subtle:#fef3c7;
  --pg-mark:#0f172a;color:var(--bs-body-color)}
.pg .card{--bs-card-border-color:#e2e8f0;--bs-card-border-radius:16px;--bs-card-spacer-x:20px;--bs-card-spacer-y:20px;box-shadow:0 1px 2px rgba(15,23,42,.04),0 8px 24px rgba(15,23,42,.05)}
.pg .progress{--bs-progress-bg:#e2e8f0;--bs-progress-bar-bg:#2a78d6;--bs-progress-height:10px;--bs-progress-border-radius:999px}
.pg .progress-bar{border-radius:999px}
.pg-title{font-size:1rem;font-weight:700}
.pg-sub{font-size:.8rem;color:var(--bs-secondary-color)}
.pg-num{font-variant-numeric:tabular-nums}
.pg .badge{display:inline-flex;align-items:center;gap:4px;padding:3px 8px 3px 6px;font-size:.74rem;font-weight:700}
.pg .badge svg{width:13px;height:13px;flex:none}
.min-w-0{min-width:0}
.pg-ring{position:relative;flex:none;width:96px;height:96px}
.pg-ring svg{width:100%;height:100%;transform:rotate(-90deg)}
.pg-ring circle{fill:none;stroke-width:10}
.pg-ring .trk{stroke:#e2e8f0}
.pg-ring .bar{stroke:#2a78d6;stroke-linecap:round;stroke-dasharray:var(--p) 100;transition:stroke-dasharray .6s ease}
.pg-ring.is-warn .bar{stroke:#d97706}
.pg-ring span{position:absolute;inset:0;display:grid;place-items:center;font-size:1.15rem;font-weight:750;font-variant-numeric:tabular-nums}
.pg-row-top{font-size:.88rem}
.pg-row-top b{font-weight:600;margin-right:auto}
.pg-pace{position:relative}
.pg-pace::after{content:"";position:absolute;left:var(--pace);top:-4px;bottom:-4px;width:2px;border-radius:2px;background:var(--pg-mark)}
.pg-key{font-size:.78rem;color:var(--bs-secondary-color)}
.pg-key i{width:2px;height:14px;background:var(--pg-mark);border-radius:2px}
.pg-big{font-size:2.2rem;font-weight:750;letter-spacing:-.02em;line-height:1.1}
.pg-big small{font-size:1rem;font-weight:600;color:var(--bs-secondary-color);letter-spacing:0}
.pg .pg-goal .progress{--bs-progress-height:14px;position:relative}
.pg-ticks{position:absolute;inset:0;pointer-events:none;background:linear-gradient(90deg,transparent calc(25% - 1px),#fff calc(25% - 1px) calc(25% + 1px),transparent 0 calc(50% - 1px),#fff 0 calc(50% + 1px),transparent 0 calc(75% - 1px),#fff 0 calc(75% + 1px),transparent 0)}
.pg-miles li{font-size:.8rem;color:var(--bs-secondary-color);text-align:right}
.pg-miles li>div{padding-top:10px;border-top:2px solid #e2e8f0}
.pg-miles li.is-done>div{border-top-color:#2a78d6}
.pg-miles strong{display:flex;justify-content:flex-end;align-items:center;gap:5px;font-size:.95rem;color:var(--bs-body-color);font-variant-numeric:tabular-nums}
.pg-miles svg{width:15px;height:15px;color:#15803d}
@media (prefers-reduced-motion:reduce){.pg-ring .bar,.pg .progress-bar{transition:none}}

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#4b5768] uppercase">Radial progress rings</span>
<div class="grid grid-cols-[repeat(auto-fit,minmax(220px,1fr))] gap-4">
  <article class="flex min-w-0 items-center gap-4 rounded-2xl border border-[#e2e8f0] bg-white p-5 shadow-[0_1px_2px_rgba(15,23,42,.04),0_8px_24px_rgba(15,23,42,.05)]">
    <div class="relative size-24 flex-none" role="progressbar" aria-labelledby="pg-r1" aria-valuemin="0" aria-valuemax="100" aria-valuenow="72" aria-valuetext="72%, $360,000 of $500,000">
      <svg class="size-full -rotate-90" viewBox="0 0 100 100" aria-hidden="true"><circle class="fill-none stroke-[#e2e8f0] [stroke-width:10]" cx="50" cy="50" r="42"/><circle class="fill-none stroke-[#2a78d6] [stroke-width:10] [stroke-linecap:round] [stroke-dasharray:72_100] transition-[stroke-dasharray] duration-600 ease-in-out motion-reduce:transition-none" cx="50" cy="50" r="42" pathLength="100"/></svg><span class="absolute inset-0 grid place-items-center text-[1.15rem] font-[750] tabular-nums" aria-hidden="true">72%</span></div>
    <div class="flex min-w-0 flex-col items-start gap-1"><h3 class="text-base font-bold" id="pg-r1">Q4 sales target</h3><p class="text-[.8rem] text-[#4b5768] tabular-nums">$360,000 of $500,000</p>
      <span class="inline-flex items-center gap-1 rounded-full bg-[#dcfce7] py-0.5 pr-2 pl-1.5 text-[.74rem] font-bold whitespace-nowrap text-[#15803d]"><svg class="size-[13px] flex-none" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>On track</span></div>
  </article>
  <article class="flex min-w-0 items-center gap-4 rounded-2xl border border-[#e2e8f0] bg-white p-5 shadow-[0_1px_2px_rgba(15,23,42,.04),0_8px_24px_rgba(15,23,42,.05)]">
    <div class="relative size-24 flex-none" role="progressbar" aria-labelledby="pg-r2" aria-valuemin="0" aria-valuemax="100" aria-valuenow="86" aria-valuetext="86%, 43 GB of 50 GB">
      <svg class="size-full -rotate-90" viewBox="0 0 100 100" aria-hidden="true"><circle class="fill-none stroke-[#e2e8f0] [stroke-width:10]" cx="50" cy="50" r="42"/><circle class="fill-none stroke-[#d97706] [stroke-width:10] [stroke-linecap:round] [stroke-dasharray:86_100] transition-[stroke-dasharray] duration-600 ease-in-out motion-reduce:transition-none" cx="50" cy="50" r="42" pathLength="100"/></svg><span class="absolute inset-0 grid place-items-center text-[1.15rem] font-[750] tabular-nums" aria-hidden="true">86%</span></div>
    <div class="flex min-w-0 flex-col items-start gap-1"><h3 class="text-base font-bold" id="pg-r2">Storage used</h3><p class="text-[.8rem] text-[#4b5768] tabular-nums">43 GB of 50 GB</p>
      <span class="inline-flex items-center gap-1 rounded-full bg-[#fef3c7] py-0.5 pr-2 pl-1.5 text-[.74rem] font-bold whitespace-nowrap text-[#b45309]"><svg class="size-[13px] flex-none" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 9v4M12 17h.01"/><path d="M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/></svg>Near limit</span></div>
  </article>
  <article class="flex min-w-0 items-center gap-4 rounded-2xl border border-[#e2e8f0] bg-white p-5 shadow-[0_1px_2px_rgba(15,23,42,.04),0_8px_24px_rgba(15,23,42,.05)]">
    <div class="relative size-24 flex-none" role="progressbar" aria-labelledby="pg-r3" aria-valuemin="0" aria-valuemax="100" aria-valuenow="91" aria-valuetext="91%, 1,183 of 1,300 accounts">
      <svg class="size-full -rotate-90" viewBox="0 0 100 100" aria-hidden="true"><circle class="fill-none stroke-[#e2e8f0] [stroke-width:10]" cx="50" cy="50" r="42"/><circle class="fill-none stroke-[#2a78d6] [stroke-width:10] [stroke-linecap:round] [stroke-dasharray:91_100] transition-[stroke-dasharray] duration-600 ease-in-out motion-reduce:transition-none" cx="50" cy="50" r="42" pathLength="100"/></svg><span class="absolute inset-0 grid place-items-center text-[1.15rem] font-[750] tabular-nums" aria-hidden="true">91%</span></div>
    <div class="flex min-w-0 flex-col items-start gap-1"><h3 class="text-base font-bold" id="pg-r3">Onboarding done</h3><p class="text-[.8rem] text-[#4b5768] tabular-nums">1,183 of 1,300 accounts</p>
      <span class="inline-flex items-center gap-1 rounded-full bg-[#dcfce7] py-0.5 pr-2 pl-1.5 text-[.74rem] font-bold whitespace-nowrap text-[#15803d]"><svg class="size-[13px] flex-none" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>On track</span></div>
  </article>
</div></div>

<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#4b5768] uppercase">Target bars with a pace marker</span>
<article class="flex flex-col gap-[18px] rounded-2xl border border-[#e2e8f0] bg-white p-5 shadow-[0_1px_2px_rgba(15,23,42,.04),0_8px_24px_rgba(15,23,42,.05)]">
  <div class="flex flex-wrap items-start justify-between gap-3"><div><h3 class="text-base font-bold">October targets</h3><p class="text-[.8rem] text-[#4b5768]">Day 16 of 31 · updated 09:40</p></div>
    <p class="flex items-center gap-2 text-[.78rem] text-[#4b5768]"><i class="h-3.5 w-0.5 rounded-sm bg-[#0f172a]" aria-hidden="true"></i>Marker: where you should be today (52%)</p></div>
  <ul class="flex flex-col gap-[18px]">
    <li><div class="mb-2 flex flex-wrap items-baseline justify-between gap-2.5 text-[.88rem]"><b class="mr-auto font-semibold" id="pg-t1">New MRR</b><span class="text-[#4b5768] tabular-nums"><strong class="text-[#0f172a]">$9,820</strong> of $15,000</span><span class="inline-flex items-center gap-1 rounded-full bg-[#dcfce7] py-0.5 pr-2 pl-1.5 text-[.74rem] font-bold whitespace-nowrap text-[#15803d]"><svg class="size-[13px] flex-none" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5M5 12l7-7 7 7"/></svg>Ahead of pace</span></div>
      <div class="relative h-2.5 rounded-full bg-[#e2e8f0] after:absolute after:-inset-y-1 after:left-[52%] after:w-0.5 after:rounded-sm after:bg-[#0f172a]" role="progressbar" aria-labelledby="pg-t1" aria-valuemin="0" aria-valuemax="15000" aria-valuenow="9820" aria-valuetext="$9,820 of $15,000, 65%, ahead of pace"><i class="absolute inset-y-0 left-0 w-[65%] rounded-full bg-[#2a78d6] transition-[width] duration-600 motion-reduce:transition-none"></i></div></li>
    <li><div class="mb-2 flex flex-wrap items-baseline justify-between gap-2.5 text-[.88rem]"><b class="mr-auto font-semibold" id="pg-t2">Enterprise demos booked</b><span class="text-[#4b5768] tabular-nums"><strong class="text-[#0f172a]">21</strong> of 40</span><span class="inline-flex items-center gap-1 rounded-full bg-[#dcfce7] py-0.5 pr-2 pl-1.5 text-[.74rem] font-bold whitespace-nowrap text-[#15803d]"><svg class="size-[13px] flex-none" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>On pace</span></div>
      <div class="relative h-2.5 rounded-full bg-[#e2e8f0] after:absolute after:-inset-y-1 after:left-[52%] after:w-0.5 after:rounded-sm after:bg-[#0f172a]" role="progressbar" aria-labelledby="pg-t2" aria-valuemin="0" aria-valuemax="40" aria-valuenow="21" aria-valuetext="21 of 40, 53%, on pace"><i class="absolute inset-y-0 left-0 w-[52.5%] rounded-full bg-[#2a78d6] transition-[width] duration-600 motion-reduce:transition-none"></i></div></li>
    <li><div class="mb-2 flex flex-wrap items-baseline justify-between gap-2.5 text-[.88rem]"><b class="mr-auto font-semibold" id="pg-t3">Newsletter signups</b><span class="text-[#4b5768] tabular-nums"><strong class="text-[#0f172a]">380</strong> of 1,000</span><span class="inline-flex items-center gap-1 rounded-full bg-[#fee2e2] py-0.5 pr-2 pl-1.5 text-[.74rem] font-bold whitespace-nowrap text-[#b91c1c]"><svg class="size-[13px] flex-none" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14M19 12l-7 7-7-7"/></svg>Behind pace</span></div>
      <div class="relative h-2.5 rounded-full bg-[#e2e8f0] after:absolute after:-inset-y-1 after:left-[52%] after:w-0.5 after:rounded-sm after:bg-[#0f172a]" role="progressbar" aria-labelledby="pg-t3" aria-valuemin="0" aria-valuemax="1000" aria-valuenow="380" aria-valuetext="380 of 1,000, 38%, behind pace"><i class="absolute inset-y-0 left-0 w-[38%] rounded-full bg-[#2a78d6] transition-[width] duration-600 motion-reduce:transition-none"></i></div></li>
    <li><div class="mb-2 flex flex-wrap items-baseline justify-between gap-2.5 text-[.88rem]"><b class="mr-auto font-semibold" id="pg-t4">Partner referrals</b><span class="text-[#4b5768] tabular-nums"><strong class="text-[#0f172a]">14</strong> of 25</span><span class="inline-flex items-center gap-1 rounded-full bg-[#dcfce7] py-0.5 pr-2 pl-1.5 text-[.74rem] font-bold whitespace-nowrap text-[#15803d]"><svg class="size-[13px] flex-none" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5M5 12l7-7 7 7"/></svg>Ahead of pace</span></div>
      <div class="relative h-2.5 rounded-full bg-[#e2e8f0] after:absolute after:-inset-y-1 after:left-[52%] after:w-0.5 after:rounded-sm after:bg-[#0f172a]" role="progressbar" aria-labelledby="pg-t4" aria-valuemin="0" aria-valuemax="25" aria-valuenow="14" aria-valuetext="14 of 25, 56%, ahead of pace"><i class="absolute inset-y-0 left-0 w-[56%] rounded-full bg-[#2a78d6] transition-[width] duration-600 motion-reduce:transition-none"></i></div></li>
  </ul>
</article></div>

<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#4b5768] uppercase">Goal with milestones</span>
<article class="flex flex-col gap-3.5 rounded-2xl border border-[#e2e8f0] bg-white p-5 shadow-[0_1px_2px_rgba(15,23,42,.04),0_8px_24px_rgba(15,23,42,.05)]">
  <div class="flex flex-wrap items-end justify-between gap-3"><div><h3 class="text-base font-bold" id="pg-goal">2026 signup goal</h3><p class="text-[2.2rem] leading-[1.1] font-[750] tracking-[-.02em] tabular-nums">8,412 <small class="text-base font-semibold tracking-normal text-[#4b5768]">of 10,000 signups</small></p></div>
    <span class="inline-flex items-center gap-1 rounded-full bg-[#dcfce7] py-0.5 pr-2 pl-1.5 text-[.74rem] font-bold whitespace-nowrap text-[#15803d]"><svg class="size-[13px] flex-none" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5M5 12l7-7 7 7"/></svg>494 ahead of plan</span></div>
  <div class="relative"><progress class="block h-3.5 w-full appearance-none overflow-hidden rounded-full border-0 bg-[#e2e8f0] [&::-moz-progress-bar]:rounded-full [&::-moz-progress-bar]:bg-[#2a78d6] [&::-webkit-progress-bar]:bg-[#e2e8f0] [&::-webkit-progress-value]:rounded-full [&::-webkit-progress-value]:bg-[#2a78d6]" max="10000" value="8412" aria-labelledby="pg-goal" aria-valuetext="8,412 of 10,000 signups, 84%">84%</progress><span class="pointer-events-none absolute inset-0 bg-[linear-gradient(90deg,transparent_calc(25%-1px),#fff_calc(25%-1px)_calc(25%+1px),transparent_0_calc(50%-1px),#fff_0_calc(50%+1px),transparent_0_calc(75%-1px),#fff_0_calc(75%+1px),transparent_0)]" aria-hidden="true"></span></div>
  <ol class="grid grid-cols-2 gap-2.5 sm:grid-cols-4" aria-label="Milestones">
    <li class="flex flex-col items-end gap-0.5 border-t-2 pt-2.5 text-right text-[.8rem] text-[#4b5768] border-[#2a78d6]"><strong class="flex items-center gap-[5px] text-[.95rem] text-[#0f172a] tabular-nums"><svg class="size-[15px] text-[#15803d]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>2,500</strong>Reached Mar 14</li>
    <li class="flex flex-col items-end gap-0.5 border-t-2 pt-2.5 text-right text-[.8rem] text-[#4b5768] border-[#2a78d6]"><strong class="flex items-center gap-[5px] text-[.95rem] text-[#0f172a] tabular-nums"><svg class="size-[15px] text-[#15803d]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>5,000</strong>Reached Jul 2</li>
    <li class="flex flex-col items-end gap-0.5 border-t-2 pt-2.5 text-right text-[.8rem] text-[#4b5768] border-[#2a78d6]"><strong class="flex items-center gap-[5px] text-[.95rem] text-[#0f172a] tabular-nums"><svg class="size-[15px] text-[#15803d]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>7,500</strong>Reached Sep 21</li>
    <li class="flex flex-col items-end gap-0.5 border-t-2 pt-2.5 text-right text-[.8rem] text-[#4b5768] border-[#e2e8f0]"><strong class="flex items-center gap-[5px] text-[.95rem] text-[#0f172a] tabular-nums">10,000</strong>1,588 to go</li>
  </ol>
</article></div>

React code

Requires: React 19, Tailwind CSS 4

JSX
// Goal and progress widgets: SVG progress rings, target bars with an expected-pace marker,
// and an annual goal on a native <progress> with milestones. Every bar is a labelled progressbar
// with a spoken value; status pills pair an icon with words, so colour is never the only signal.
// Usage: <ProgressGoals rings={[…]} targets={{…}} goal={{…}} /> — all props have demo data.
import { useId } from 'react';

const STATUS = {
  onTrack: { cls: 'bg-[#dcfce7] text-[#15803d]', icon: <path d="M20 6 9 17l-5-5" /> },
  ahead: { cls: 'bg-[#dcfce7] text-[#15803d]', icon: <path d="M12 19V5M5 12l7-7 7 7" /> },
  behind: { cls: 'bg-[#fee2e2] text-[#b91c1c]', icon: <path d="M12 5v14M19 12l-7 7-7-7" /> },
  warning: { cls: 'bg-[#fef3c7] text-[#b45309]', icon: <><path d="M12 9v4M12 17h.01" /><path d="M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z" /></> },
};

const defaultRings = [
  { title: 'Q4 sales target', percent: 72, detail: '$360,000 of $500,000', color: 'stroke-[#2a78d6]', status: 'onTrack', statusText: 'On track' },
  { title: 'Storage used', percent: 86, detail: '43 GB of 50 GB', color: 'stroke-[#d97706]', status: 'warning', statusText: 'Near limit' },
  { title: 'Onboarding done', percent: 91, detail: '1,183 of 1,300 accounts', color: 'stroke-[#2a78d6]', status: 'onTrack', statusText: 'On track' },
];

const defaultTargets = {
  title: 'October targets', subtitle: 'Day 16 of 31 · updated 09:40', pace: 52,
  items: [
    { label: 'New MRR', value: 9820, max: 15000, format: 'usd', status: 'ahead', statusText: 'Ahead of pace' },
    { label: 'Enterprise demos booked', value: 21, max: 40, status: 'onTrack', statusText: 'On pace' },
    { label: 'Newsletter signups', value: 380, max: 1000, status: 'behind', statusText: 'Behind pace' },
    { label: 'Partner referrals', value: 14, max: 25, status: 'ahead', statusText: 'Ahead of pace' },
  ],
};

const defaultGoal = {
  title: '2026 signup goal', value: 8412, max: 10000, unit: 'signups', status: 'ahead', statusText: '494 ahead of plan',
  milestones: [
    { value: 2500, note: 'Reached Mar 14', reached: true },
    { value: 5000, note: 'Reached Jul 2', reached: true },
    { value: 7500, note: 'Reached Sep 21', reached: true },
    { value: 10000, note: '1,588 to go' },
  ],
};

const n = new Intl.NumberFormat('en-US');
const usd = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 });
const card = 'rounded-2xl border border-[#e2e8f0] bg-white p-5 shadow-[0_1px_2px_rgba(15,23,42,.04),0_8px_24px_rgba(15,23,42,.05)]';
const label = 'mb-4 block text-[.72rem] tracking-[.12em] text-[#4b5768] uppercase';

function StatusPill({ status, children }) {
  const s = STATUS[status];
  return (
    <span className={`inline-flex items-center gap-1 rounded-full py-0.5 pr-2 pl-1.5 text-[.74rem] font-bold whitespace-nowrap ${s.cls}`}>
      <svg className="size-[13px] flex-none" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">{s.icon}</svg>
      {children}
    </span>
  );
}

function ProgressRing({ ring: r }) {
  const titleId = useId();
  return (
    <article className={`flex min-w-0 items-center gap-4 ${card}`}>
      <div className="relative size-24 flex-none" role="progressbar" aria-labelledby={titleId} aria-valuemin={0} aria-valuemax={100} aria-valuenow={r.percent} aria-valuetext={`${r.percent}%, ${r.detail}`}>
        <svg className="size-full -rotate-90" viewBox="0 0 100 100" aria-hidden="true">
          <circle className="fill-none stroke-[#e2e8f0] [stroke-width:10]" cx="50" cy="50" r="42" />
          <circle className={`fill-none ${r.color} [stroke-width:10] [stroke-linecap:round] transition-[stroke-dasharray] duration-600 ease-in-out motion-reduce:transition-none`} style={{ strokeDasharray: `${r.percent} 100` }} cx="50" cy="50" r="42" pathLength="100" />
        </svg>
        <span className="absolute inset-0 grid place-items-center text-[1.15rem] font-[750] tabular-nums" aria-hidden="true">{r.percent}%</span>
      </div>
      <div className="flex min-w-0 flex-col items-start gap-1">
        <h3 className="text-base font-bold" id={titleId}>{r.title}</h3>
        <p className="text-[.8rem] text-[#4b5768] tabular-nums">{r.detail}</p>
        <StatusPill status={r.status}>{r.statusText}</StatusPill>
      </div>
    </article>
  );
}

function TargetBar({ item: t, pace }) {
  const labelId = useId();
  const fmt = t.format === 'usd' ? usd : n;
  const pct = (t.value / t.max) * 100;
  return (
    <li>
      <div className="mb-2 flex flex-wrap items-baseline justify-between gap-2.5 text-[.88rem]">
        <b className="mr-auto font-semibold" id={labelId}>{t.label}</b>
        <span className="text-[#4b5768] tabular-nums"><strong className="text-[#0f172a]">{fmt.format(t.value)}</strong> of {fmt.format(t.max)}</span>
        <StatusPill status={t.status}>{t.statusText}</StatusPill>
      </div>
      <div
        className="relative h-2.5 rounded-full bg-[#e2e8f0] after:absolute after:-inset-y-1 after:left-(--pace) after:w-0.5 after:rounded-sm after:bg-[#0f172a]"
        style={{ '--pace': `${pace}%` }}
        role="progressbar" aria-labelledby={labelId} aria-valuemin={0} aria-valuemax={t.max} aria-valuenow={t.value}
        aria-valuetext={`${fmt.format(t.value)} of ${fmt.format(t.max)}, ${Math.round(pct)}%, ${t.statusText.toLowerCase()}`}
      >
        <i className="absolute inset-y-0 left-0 rounded-full bg-[#2a78d6] transition-[width] duration-600 motion-reduce:transition-none" style={{ width: `${pct}%` }} />
      </div>
    </li>
  );
}

function GoalCard({ goal: g }) {
  const titleId = useId();
  const pct = Math.round((g.value / g.max) * 100);
  return (
    <article className={`flex flex-col gap-3.5 ${card}`}>
      <div className="flex flex-wrap items-end justify-between gap-3">
        <div>
          <h3 className="text-base font-bold" id={titleId}>{g.title}</h3>
          <p className="text-[2.2rem] leading-[1.1] font-[750] tracking-[-.02em] tabular-nums">{n.format(g.value)} <small className="text-base font-semibold tracking-normal text-[#4b5768]">of {n.format(g.max)} {g.unit}</small></p>
        </div>
        <StatusPill status={g.status}>{g.statusText}</StatusPill>
      </div>
      <div className="relative">
        <progress className="block h-3.5 w-full appearance-none overflow-hidden rounded-full border-0 bg-[#e2e8f0] [&::-moz-progress-bar]:rounded-full [&::-moz-progress-bar]:bg-[#2a78d6] [&::-webkit-progress-bar]:bg-[#e2e8f0] [&::-webkit-progress-value]:rounded-full [&::-webkit-progress-value]:bg-[#2a78d6]" max={g.max} value={g.value} aria-labelledby={titleId} aria-valuetext={`${n.format(g.value)} of ${n.format(g.max)} ${g.unit}, ${pct}%`}>{pct}%</progress>
        <span className="pointer-events-none absolute inset-0 bg-[linear-gradient(90deg,transparent_calc(25%-1px),#fff_calc(25%-1px)_calc(25%+1px),transparent_0_calc(50%-1px),#fff_0_calc(50%+1px),transparent_0_calc(75%-1px),#fff_0_calc(75%+1px),transparent_0)]" aria-hidden="true" />
      </div>
      <ol className="grid grid-cols-2 gap-2.5 sm:grid-cols-4" aria-label="Milestones">
        {g.milestones.map((m) => (
          <li key={m.value} className={`flex flex-col items-end gap-0.5 border-t-2 pt-2.5 text-right text-[.8rem] text-[#4b5768] ${m.reached ? 'border-[#2a78d6]' : 'border-[#e2e8f0]'}`}>
            <strong className="flex items-center gap-[5px] text-[.95rem] text-[#0f172a] tabular-nums">
              {m.reached && (
                <svg className="size-[15px] text-[#15803d]" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5" /></svg>
              )}
              {n.format(m.value)}
            </strong>
            {m.note}
          </li>
        ))}
      </ol>
    </article>
  );
}

export default function ProgressGoals({ rings = defaultRings, targets = defaultTargets, goal = defaultGoal }) {
  return (
    <div className="flex flex-col gap-[46px]">
      <div><span className={label}>Radial progress rings</span>
        <div className="grid grid-cols-[repeat(auto-fit,minmax(220px,1fr))] gap-4">
          {rings.map((r) => <ProgressRing key={r.title} ring={r} />)}
        </div>
      </div>
      <div><span className={label}>Target bars with a pace marker</span>
        <article className={`flex flex-col gap-[18px] ${card}`}>
          <div className="flex flex-wrap items-start justify-between gap-3">
            <div><h3 className="text-base font-bold">{targets.title}</h3><p className="text-[.8rem] text-[#4b5768]">{targets.subtitle}</p></div>
            <p className="flex items-center gap-2 text-[.78rem] text-[#4b5768]"><i className="h-3.5 w-0.5 rounded-sm bg-[#0f172a]" aria-hidden="true" />Marker: where you should be today ({targets.pace}%)</p>
          </div>
          <ul className="flex flex-col gap-[18px]">
            {targets.items.map((t) => <TargetBar key={t.label} item={t} pace={targets.pace} />)}
          </ul>
        </article>
      </div>
      <div><span className={label}>Goal with milestones</span>
        <GoalCard goal={goal} />
      </div>
    </div>
  );
}

About this set

Widgets for tracking a number against a target. Radial progress rings shows three cards with an SVG ring each: a quarterly sales target, storage used and onboarding completion, with the percentage in the middle, the raw figures underneath and a status badge such as On track or Near limit. Target bars with a pace marker lists October targets as horizontal bars, each with a thin marker for where the team should be on day 16 of 31, so ahead of pace and behind pace are visible at a glance and also written in a badge. Goal with milestones tracks an annual signup goal with a native progress element divided into quarters, a large current count and a milestone row showing which steps were reached and when. Use rings for a few headline goals, bars for a list of targets, and the milestone card for one long-running goal. The rings use pathLength so the percentage maps straight onto stroke-dasharray, and every ring and bar is a named progressbar with a value text. No JavaScript.

What’s included

  • SVG progress rings driven by a single percentage
  • Target bars with an expected-pace marker
  • Native progress element with milestone ticks
  • Status written as a word with an icon, never colour alone
  • Every bar and ring is a labelled progressbar with a value text
  • Bootstrap version uses .progress and .badge

Accessibility

Rings and target bars use role="progressbar" with aria-valuemin, aria-valuemax, aria-valuenow and an aria-valuetext such as "$9,820 of $15,000, 65%, ahead of pace", named by their visible heading. The goal uses a native progress element. Status badges pair an icon with a word, and fill transitions are switched off under prefers-reduced-motion.

Customise it

Change --pg-fill, --pg-track and --pg-mark on .pg for the bar, track and pace marker, and set --p (and --pace) on each ring or bar. In Bootstrap set --bs-progress-bar-bg on .progress; in Tailwind change the bg- and stroke- colour classes.