jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

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-dasharraystroke-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}

  1. 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 trong rotate(-90 cx cy) để tiến trình bắt đầu từ trên.}
  2. Round caps. stroke-linecap: round gives that soft, modern fill end. {Đầu bo tròn. stroke-linecap: round cho đầ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 needs C = 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=80 paints from 74→86; shrink r if 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ên r=80 sơn từ 74→86; thu r nế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 khi rotate() làm được.}

Practice, or it didn’t happen {Luyện tập, không thì coi như chưa học}

  1. 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.}
  2. Countdown ring {Vòng đếm ngược}: drive stroke-dashoffset from a timer so the ring empties over 10 seconds. {lái stroke-dashoffset từ một timer để vòng cạn dần trong 10 giây.}
  3. 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.}