Progress Ring

프로그레스 링

A circular outline that fills to an exact percentage — a determinate loader.

Also known as: Circular progressDeterminate ring
···
html
<div class="wrap">
  <div class="pr" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0" aria-live="polite">
    <svg viewBox="0 0 60 60" aria-hidden="true"><circle cx="30" cy="30" r="26" fill="none" stroke="var(--line)" stroke-width="5"/><circle class="prc" cx="30" cy="30" r="26" fill="none" stroke="var(--accent-3)" stroke-width="5" stroke-linecap="round"/></svg>
    <span class="prn">0%</span>
  </div>
  <span class="cap">stroke-dashoffset = 둘레 &times; (1 &minus; 진행률)</span>
</div>
css
.wrap{display:flex;flex-direction:column;align-items:center;gap:16px}
.pr{position:relative;width:clamp(46px,15vmin,84px);height:clamp(46px,15vmin,84px);display:grid;place-items:center}
.pr svg{position:absolute;inset:0;width:100%;height:100%;transform:rotate(-90deg)}
.prc{stroke-dasharray:163.4;transition:stroke-dashoffset .25s linear}
.prn{font-size:clamp(11px,3.2vmin,16px);font-weight:700;font-variant-numeric:tabular-nums}
.cap{font-size:clamp(9px,2.4vmin,11px);color:var(--muted);letter-spacing:.02em}
js
const C = 163.4;
const circle = document.querySelector('.prc');
const num = document.querySelector('.prn');
const wrap = document.querySelector('.pr');
let pct = 0;
function tick() {
  pct = (pct + 2) % 101;
  circle.style.strokeDashoffset = String(C * (1 - pct / 100));
  num.textContent = pct + '%';
  wrap.setAttribute('aria-valuenow', String(pct));
}
tick();
setInterval(tick, 60);

The one determinate loader in this gallery. Use it only when remaining work can actually be calculated — a file upload, a download, an install — because showing a number you can't back up (a fake progress value) burns trust. The reverse also holds: using an indeterminate loader like a spinner or dots when progress is knowable hides information the user could otherwise see, which feels needlessly opaque.

An SVG <circle> sets its full circumference (2πr) via stroke-dasharray, and stroke-dashoffset is computed as "circumference × (1 − percent)" so only that fraction of the ring appears drawn. Showing the same number as text in the center matters for anyone with low vision or color-vision differences who can't rely on the ring's fill alone.

This shape suits progress for one region — a card, an inline button — rather than a full-screen load; for several files processed in sequence, showing "done so far out of total" beats one ring per file. Update role="progressbar" with aria-valuenow, aria-valuemin="0" and aria-valuemax="100" live, and reflect the same percentage as text inside an aria-live="polite" region. Under prefers-reduced-motion, shorten the fill transition or update the value instantly instead of animating it.