SVG from Zero to Senior · Part 12 — Progress Rings & Gauges
Build the loaders, percentage rings, and speedometers in every dashboard with one formula: stroke-dashoffset = circumference × (1 − percent). Plus the -90° start rotation, round caps, gradient rings, and a 270° gauge. With a live builder.
Some components look advanced but rest on a single elegant formula. {Có những component trông cao siêu nhưng dựa trên một công thức thanh lịch duy nhất.} The circular progress ring — that percentage loader in every dashboard, fitness app, and checkout — is exactly this. {Vòng tiến trình tròn — cái loader phần trăm trong mọi dashboard, app thể dục, và trang thanh toán — chính là vậy.} Once you see the trick you’ll build them in your sleep. {Một khi thấy được mánh con sẽ dựng chúng trong lúc ngủ.}
Drag percent, width, and color; then explore the 270° gauge below. {Kéo phần trăm, độ rộng, và màu; rồi khám phá đồng hồ 270° bên dưới.}
Open the full demo {Mở demo đầy đủ}: /tools/svg-gauge-demo/.
The one formula {Một công thức}
Remember stroke-dasharray and stroke-dashoffset from Part 3 and 6? {Nhớ stroke-dasharray và stroke-dashoffset ở Phần 3 và 6 chứ?} Here’s their killer application. {Đây là ứng dụng đỉnh của chúng.}
Take a stroked circle. Its outline length is the circumference: {Lấy một vòng tròn có stroke. Chiều dài đường viền là chu vi:}
C = 2 × π × r
Now make the dash pattern one dash exactly as long as the whole circle (stroke-dasharray: C). {Giờ làm pattern nét đứt thành một nét dài đúng bằng cả vòng (stroke-dasharray: C).} The dash covers the entire ring. {Nét đó phủ cả vòng.} Then push it away with stroke-dashoffset: {Rồi đẩy nó đi bằng stroke-dashoffset:}
dashoffset = C × (1 − percent)
percent = 0→ offset = C → the whole dash is shifted out of view → empty ring. {cả nét bị đẩy ra → vòng rỗng.}percent = 1→ offset = 0 → the dash sits fully on the ring → complete. {nét nằm trọn trên vòng → đầy.}
<svg viewBox="0 0 200 200">
<g transform="rotate(-90 100 100)"> <!-- start at 12 o'clock -->
<circle cx="100" cy="100" r="80" fill="none" stroke="#232323" stroke-width="12"/>
<circle cx="100" cy="100" r="80" fill="none" stroke="#c8ff00" stroke-width="12"
stroke-linecap="round"
stroke-dasharray="502.6" /* C = 2π·80 */
stroke-dashoffset="160.8" /> /* C × (1 − 0.68) */
</g>
</svg>
const C = 2 * Math.PI * r;
fill.style.strokeDasharray = C;
fill.style.strokeDashoffset = C * (1 - percent); // percent in 0..1
That’s the entire component. {Đó là toàn bộ component.} Everything else is polish. {Phần còn lại là đánh bóng.}
The two details that make it look right {Hai chi tiết làm nó trông đúng}
- Start at 12 o’clock. A circle’s stroke starts at 3 o’clock (the positive X-axis). Wrap it in
<g transform="rotate(-90 cx cy)">so progress begins at the top, like every real ring. {Bắt đầu ở 12 giờ. Stroke của vòng tròn bắt đầu ở 3 giờ. Bọc trongrotate(-90 cx cy)để tiến trình bắt đầu từ trên.} - Round caps.
stroke-linecap: roundgives that soft, modern fill end. {Đầu bo tròn.stroke-linecap: roundcho đầu nét mềm, hiện đại.}
Animate it smoothly {Animate nó mượt}
Because stroke-dashoffset is a single animatable number, a one-line CSS transition makes the ring glide to its new value: {Vì stroke-dashoffset là một số animate được, một transition CSS một dòng làm vòng trượt tới giá trị mới:}
.ring-fill {
transition: stroke-dashoffset 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
Update the offset in JS and it animates for free — no keyframes needed. {Cập nhật offset trong JS và nó tự animate — không cần keyframe.} For a determinate→indeterminate spinner, rotate the whole SVG with a CSS @keyframes spin while keeping a fixed dash gap. {Cho spinner xác định→không xác định, xoay cả SVG bằng @keyframes spin trong khi giữ khoảng hở dash cố định.}
Gradient rings & conic alternative {Vòng gradient & lựa chọn conic}
Want the fill to shift color along the arc? {Muốn màu fill đổi dọc cung?} A linearGradient on the stroke works for a two-tone look; for a true rainbow-around-the-ring you either rotate a gradient or switch to a CSS conic-gradient mask. {Một linearGradient trên stroke cho vẻ hai tông; cho cầu vồng quanh vòng thật con xoay gradient hoặc chuyển sang mask conic-gradient của CSS.} For pure progress, the SVG stroke approach wins on crispness and animation control. {Cho tiến trình thuần, cách stroke SVG thắng về độ sắc và kiểm soát animation.}
The 270° gauge (speedometer) {Đồng hồ 270° (tốc kế)}
A gauge is the same idea, but the track only covers part of the circle. {Đồng hồ cùng ý tưởng, nhưng track chỉ phủ một phần vòng tròn.} Use a dasharray of [arcLength, hugeGap] so only the arc shows, rotate the group so the gap sits at the bottom, and the fill is arcLength × value: {Dùng dasharray [độ-dài-cung, khoảng-hở-lớn] để chỉ cung hiện, xoay nhóm để khoảng hở nằm dưới, và fill là độ-dài-cung × giá-trị:}
const C = 2 * Math.PI * r;
const arc = C * 0.75; // 270° = three-quarters
track.style.strokeDasharray = `${arc} ${C}`; // show 270°, hide the rest
fill.style.strokeDasharray = `${arc * value} ${C}`;
// rotate the group by 135° so the open quarter is centred at the bottom
Same circumference math, one extra rotation — and you have a speedometer, battery meter, or score dial. {Cùng toán chu vi, thêm một phép xoay — và con có tốc kế, đồng hồ pin, hay mặt số điểm.}
The master’s warnings {Lời cảnh báo của sư phụ}
- Match the radius in the formula and the markup. A
r="80"circle needsC = 2π·80; using the wrong radius is the #1 “my ring never fills” bug. {Khớp bán kính trong công thức và markup. Sai bán kính là lỗi số 1 “vòng không bao giờ đầy”.} - Account for stroke width in sizing. The stroke straddles the radius, so a 12px stroke on
r=80paints from 74→86; shrinkrif it clips the viewBox. {Tính stroke width khi định cỡ. Stroke cưỡi lên bán kính, nên stroke 12px trênr=80sơn từ 74→86; thurnếu nó cắt viewBox.} - Rotate, don’t redraw, to change the start angle. Reaching for a custom arc path is overkill when
rotate()does it. {Xoay, đừng vẽ lại, để đổi góc bắt đầu. Vẽ arc path riêng là thừa khirotate()làm được.}
Practice, or it didn’t happen {Luyện tập, không thì coi như chưa học}
- 68% ring {Vòng 68%}: build the ring, compute C in JS, and animate it from 0 → 68% on load. {dựng vòng, tính C trong JS, và animate từ 0 → 68% khi tải.}
- Countdown ring {Vòng đếm ngược}: drive
stroke-dashoffsetfrom a timer so the ring empties over 10 seconds. {láistroke-dashoffsettừ một timer để vòng cạn dần trong 10 giây.} - Battery gauge {Đồng hồ pin}: a 180° gauge that turns the fill red below 20%. {một gauge 180° đổi fill sang đỏ khi dưới 20%.}
What’s next {Phần tiếp theo}
You can now build precise circular meters. {Giờ con dựng được đồng hồ tròn chính xác.} Next we get playful and generative. {Tiếp theo ta nghịch ngợm và sinh tạo.} In Part 13 we make generative patterns and SVG backgrounds — repeating <pattern> tiles, organic blob generators, and the lightweight hero backgrounds that ship as tiny data-URIs instead of heavy images. {Ở Phần 13 ta làm pattern sinh tạo và nền SVG — ô <pattern> lặp, máy sinh blob hữu cơ, và các nền hero nhẹ ship dưới dạng data-URI tí hon thay cho ảnh nặng.}