GODRICH
#css-animation#button#loader#transition#scroll#ui#xu-huong-thiet-ke

9 CSS Progress Bar Circle Gauges — chỉ cần đổi số

CSS progress bar circle là thanh và vòng tiến trình chỉ cần đổi một con số để vẽ lại toàn bộ.

Tự chạy · bấm ô để tới mục đó · cả 9 trong một zip

Thứ tự chín kiểu này đi theo độ khó của phép tính đứng sau con số, không theo mức phổ biến. 01 và 02 chỉ cần một thanh đổi theo một trục. 03 và 04 chuyển từ hình chữ nhật sang hình tròn — một bên là vòng đầy đủ, một bên chỉ còn nửa vòng có kim chỉ. 05 và 06 bỏ hẳn thanh liền mạch, một bên tách thành từng khối rời, một bên không có giá trị cố định nào cả. 07 gắn thêm một con số di chuyển đúng theo mép phần đã đầy. 08 nối nhiều mốc lại thành một chuỗi bước, còn 09 là kiểu duy nhất đi lùi, cạn dần thay vì đầy lên. Mỗi khối mã dưới đây chỉ giữ phần trực tiếp tạo ra chuyển động, bản đầy đủ nằm trong file zip.

01Thanh chạy đầy cơ bản

Bạn thấy gì

--percent được đăng ký qua @property với kiểu number nên trình duyệt nội suy mượt giữa các khung hình, còn phần chạy đầy bên trong không đổi width mà chỉ co giãn theo transform: scaleX().

Thanh chạy đầy cơ bản
Mở trong tab mới

01 · Thanh chạy đầy cơ bản — Diện tích đo được 2.051% · khung hình chuyển động 15/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

scaleX@property2초 루프

Dùng ở đâu

Đây là kiểu dựng đơn giản nhất trong cả chín kiểu, hợp cho thanh tải file lên hay tiến trình gửi form khi chỉ cần đúng một con số.

@property --percent { syntax: "<number>"; inherits: true; initial-value: 0; }
.track { --percent: 0; position: relative; overflow: hidden; }
.bar {
  position: absolute; inset: 0; background: $color;
  transform: scaleX(calc(var(--percent) / 100));
  transform-origin: left center;
}
.track.is-demo { animation: fill $duration $easing infinite; }
@keyframes fill { 0% { --percent: 0; } 100% { --percent: 76; } }

02Sọc chạy animation

Chuyện gì diễn ra trên màn hình

Giá trị đứng yên cố định, chỉ riêng nền sọc chéo tiếp tục trôi qua background-position để không trông như trang đang bị treo giữa chừng.

Sọc chạy animation
Mở trong tab mới

02 · Sọc chạy animation — Diện tích đo được 1.883% · khung hình chuyển động 23/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

background-positionrepeating-linear-gradient1초 루프

Chỗ dùng

Kiểu này hợp với xử lý dữ liệu hàng loạt hay thanh đồng bộ nền — những việc chạy chậm nhưng vẫn cần một dấu hiệu cho thấy nó vẫn đang chạy.

.bar {
  position: absolute; inset: 0;
  background-image: repeating-linear-gradient(45deg, $color 0 10px, rgba($color, .55) 10px 20px);
  background-size: 28px 28px;
}
.bar.is-demo { animation: stripe-move $duration $easing infinite; }
@keyframes stripe-move {
  from { background-position: 0 0; }
  to   { background-position: 28px 0; }
}

03Vòng gauge conic-gradient

Chuyển động thế nào

Vòng donut được vẽ bằng conic-gradient, còn góc tô màu tính thẳng từ --percent qua calc() nên không cần vẽ thêm một lớp SVG nào.

Vòng gauge conic-gradient
Mở trong tab mới

03 · Vòng gauge conic-gradient — Diện tích đo được 3.458% · khung hình chuyển động 23/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

conic-gradient색 구간 전환@property

Dùng khi nào

Ba mốc trong cùng animation đổi màu --zone từ xanh dương sang vàng rồi sang màu mực tối mà không hiện một chữ số nào, để mắt nhận ra trạng thái nhanh hơn đọc số. conic-gradient() đã chạy ổn định trên Chrome, Safari và Firefox từ nhiều năm nay theo bảng hỗ trợ trên caniuse.

@property --percent { syntax: "<number>"; inherits: false; initial-value: 0; }
.ring {
  --percent: 0; --zone: #{$color};
  background: conic-gradient(var(--zone) calc(var(--percent) * 1%), rgba(255, 255, 255, .35) 0);
}
.ring.is-demo { animation: sweep $duration $easing infinite, zone $duration steps(1) infinite; }
@keyframes zone {
  40%  { --zone: #{$color}; }
  41%  { --zone: #{$stage-yellow}; }
  75%  { --zone: #{$subject-ink}; }
}

04Gauge nửa vòng tròn

Bạn thấy gì

Chiều cao khung chỉ bằng nửa chiều rộng, và overflow: hidden cắt bỏ nửa dưới của một vòng tròn đầy đủ nên chỉ nửa trên hiện ra như mặt đồng hồ tốc độ.

Gauge nửa vòng tròn
Mở trong tab mới

04 · Gauge nửa vòng tròn — Diện tích đo được 7.955% · khung hình chuyển động 23/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

transform: rotate()반원 clip바늘

Dùng ở đâu

Kim xoay bằng transform: rotate() theo đúng --percent mà vòng cung phía sau cũng dùng, nên hai thứ luôn khớp nhau vì cùng đọc một giá trị.

@property --percent { syntax: "<number>"; inherits: true; initial-value: 0; }
.gauge { --percent: 0; overflow: hidden; }
.gauge::before {
  width: 100%; height: 200%; border-radius: 50%;
  background: conic-gradient(from -90deg, $color calc(var(--percent) * 1.8deg), rgba($subject-ink, .12) 0);
}
.gauge__needle {
  transform-origin: bottom center;
  transform: translateX(-50%) rotate(calc(var(--percent) * 1.8deg - 90deg));
}

05Thanh chia đoạn theo bậc

Chuyện gì diễn ra trên màn hình

Track ở đây không phải một thanh liền mà là năm khối riêng, mỗi khối trễ nhau khoảng 160ms qua animation-delay xếp theo nth-child nên chúng sáng lên lần lượt thay vì cùng lúc.

Thanh chia đoạn theo bậc
Mở trong tab mới

05 · Thanh chia đoạn theo bậc — Diện tích đo được 2.794% · khung hình chuyển động 20/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

opacity 단계 전환animation-delay조각 5개

Chỗ dùng

Kiểu này hợp khi giá trị vốn đã là bậc thang — cường độ tín hiệu hay số mạng còn lại — chứ không phải một tỉ lệ phần trăm liên tục.

.seg {
  width: 16px; height: 46px; border-radius: 4px;
  opacity: .35; transform: scaleY(.7); transform-origin: bottom;
}
.segs.is-demo .seg { animation: light 2s $easing infinite; }
.segs.is-demo .seg:nth-child(2) { animation-delay: 160ms; }
@keyframes light {
  0%, 60%, 100% { opacity: .35; transform: scaleY(.7); }
  20%, 45%      { opacity: 1;   transform: scaleY(1); background: $color; }
}

06Thanh không xác định

Chuyển động thế nào

Không có --percent nào ở đây vì giá trị vốn không biết trước; hai đoạn thanh dài ngắn khác nhau chạy lệch nhau khoảng 35% chu kỳ để trông như đang lăn qua nhau, giống thanh loading của Material Design.

Thanh không xác định
Mở trong tab mới

06 · Thanh không xác định — Diện tích đo được 1.542% · khung hình chuyển động 19/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

두 세그먼트animation-delay 차등cubic-bezier

Dùng khi nào

Dùng khi trang vừa mới tải hoặc đang đợi kết quả tìm kiếm mà chưa có con số nào để hiển thị.

.seg {
  position: absolute; inset: 0;
  background: $color; transform-origin: left center;
}
.seg--a.is-demo { animation: seg-a $duration $easing infinite; }
.seg--b.is-demo { animation: seg-b $duration $easing infinite; animation-delay: $duration * .35; }
@keyframes seg-a {
  0%   { transform: translateX(-100%) scaleX(.15); }
  55%  { transform: translateX(30%)   scaleX(.7); }
  100% { transform: translateX(120%)  scaleX(.15); }
}

07Nhãn phần trăm đồng bộ

Bạn thấy gì

--percent được đăng ký kiểu integer vì counter() cần một số nguyên sạch để vẽ ra chữ, còn container-type: inline-size trên track cho phép nhãn bong bóng tính vị trí bằng đơn vị cqw thay vì một mốc phần trăm cố định.

Nhãn phần trăm đồng bộ
Mở trong tab mới

07 · Nhãn phần trăm đồng bộ — Diện tích đo được 7.601% · khung hình chuyển động 14/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

translateX 동기말풍선CSS 변수

Dùng ở đâu

Nhãn đó bám đúng mép thanh đang chạy vì dùng chung một --percent với chính thanh chứ không phải một biến riêng.

@property --percent { syntax: "<integer>"; inherits: true; initial-value: 0; }
.track { --percent: 0; container-type: inline-size; }
.bar { transform: scaleX(calc(var(--percent) / 100)); transform-origin: left center; }
.bubble {
  position: absolute; top: -30px; left: 0;
  transform: translateX(calc(var(--percent) * 1cqw - 50%));
}
.bubble__num { counter-reset: p var(--percent); &::before { content: counter(p) "%"; } }

08Tiến trình dạng stepper

Chuyện gì diễn ra trên màn hình

Ba vòng tròn đánh số nối bằng một đường thẳng, và animation-delay xếp chồng theo thứ tự vòng-đường-vòng-đường-vòng nên bước sau chỉ tô màu khi bước trước đã xong.

Tiến trình dạng stepper
Mở trong tab mới

08 · Tiến trình dạng stepper — Diện tích đo được 2.911% · khung hình chuyển động 23/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

scaleX 연결선원 채움단계 3개

Chỗ dùng

Khác với tám kiểu còn lại, đây không đo một tỉ lệ phần trăm liên tục mà đánh dấu mốc rời rạc, nên mã HTML dùng role="list" thay vì role="progressbar".

.stepper.is-demo .step__dot { animation: dot-fill $duration $easing both; }
.stepper.is-demo .line { animation: line-grow $duration linear both; }
.stepper.is-demo > :nth-child(2) { animation-delay: 400ms; }
.stepper.is-demo > :nth-child(3) .step__dot { animation-delay: 800ms; }
@keyframes dot-fill {
  from { background: rgba(255, 255, 255, .14); transform: scale(.85); }
  to   { background: $color; transform: scale(1); }
}
@keyframes line-grow { from { transform: scaleX(0); } to { transform: scaleX(1); } }

09Gauge hình pin

Chuyển động thế nào

Phần chạy đầy bên trong khung hình pin co theo scaleX khi --percent giảm dần từ 100 xuống một mức thấp, còn màu tự đổi từ kem sang vàng rồi sang đỏ gạch tại từng ngưỡng trong cùng một @keyframes.

Gauge hình pin
Mở trong tab mới

09 · Gauge hình pin — Diện tích đo được 1.205% · khung hình chuyển động 23/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

scaleX색 임계값 전환테두리 펄스

Dùng khi nào

Chỉ khi mức đã cạn hẳn, viền pin mới nhấp nháy nhẹ để báo thêm ngoài màu sắc, chứ không nhấp nháy suốt.

@property --percent { syntax: "<number>"; inherits: false; initial-value: 100; }
.battery__fill {
  --percent: 100;
  transform: scaleX(calc(var(--percent) / 100));
  animation: drain $duration $easing infinite;
}
@keyframes drain {
  0%   { --percent: 100; background: #{$color}; }
  55%  { --percent: 35;  background: #{$stage-yellow}; }
  100% { --percent: 8;   background: #{$error}; }
}

Chỗ dễ hỏng ở đâu? — cái bẫy

Hai chỗ làm thanh đứng im

Trong chín kiểu này có hai chỗ thật sự làm nó đứng im nếu chép nhầm. Ở 06, hai đoạn thanh đặt position: absolute nhưng không tự khai left/right, nên một phần tử absolute rỗng mà thiếu inset: 0 thì co về đúng kích thước nội dung của nó — tức 0 — và scaleX khi đó không có gì để giãn ra cả; phải ép inset: 0 phủ hết track thì phần tử mới có chiều rộng thật để transform tác động lên. Ở 04 và 07, chính .gauge và .track là nơi đặt và animate --percent, còn kim với bong bóng là những phần tử con khác đọc lại giá trị đó — theo tài liệu @property của MDN, một custom property chỉ truyền xuống được phần tử con khi đăng ký với inherits: true, đặt nhầm thành false thì con vẫn kẹt ở giá trị ban đầu trong khi cha đã chạy xong việc của nó. Cú pháp @property này chỉ chạy từ Chrome·Edge 85, Safari 16.4 và Firefox 128 trở lên — bảng chi tiết nằm ở caniuse — và trên bản cũ hơn animation bị bỏ qua hoàn toàn nên vòng hay thanh hiện thẳng giá trị cuối chứ không đếm dần lên. Chín file trong gói đã canh đúng inherits cho từng trường hợp, còn mật khẩu để mở file nén là betrch52 và bạn gõ đúng y như nó hiện trên dòng chữ này.

Khả năng tiếp cận

Vai trò progressbar và các giá trị aria

Tám trong chín kiểu — trừ 08 — mang role="progressbar". Bảy kiểu biết trước con số (01–05, 07, 09) đặt đủ aria-valuemin, aria-valuemax và aria-valuenow; riêng 06 chỉ giữ role="progressbar" cùng aria-label, bỏ cả ba thuộc tính giá trị đó vì tiến trình của nó vốn không xác định.

<div class="track" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="76" aria-label="Đang tải lên">
  <div class="bar is-demo"></div>
</div>

Ba mốc rời rạc và giảm chuyển động

Riêng 08 dùng role="list" cho khối và role="listitem" cho từng bước, vì nó đánh dấu ba mốc rời rạc chứ không phải một thang đo liên tục. Khi bật prefers-reduced-motion: reduce, cả chín kiểu dừng animation và đứng thẳng ở trạng thái cuối — 09 và 03 giữ nguyên màu ứng với mức đang chạy, còn 06 chỉ còn một thanh mờ đứng yên chứ không lăn qua lại nữa. Trang prefers-reduced-motion trên MDN ghi rõ mức hỗ trợ trình duyệt của media query này.

Các kiểu progress bar và gauge khác nằm trong chuyên mục CSS của site, còn lý do mỗi bài đóng gói thành một file zip có khoá thì đọc ở trang giới thiệu.

FAQ

Không dùng @property thì chín gauge này có còn chạy được không?

Chạy được nhưng con số sẽ nhảy thẳng một bước chứ không mượt, vì trình duyệt chỉ nội suy được một custom property khi nó được đăng ký kiểu số qua @property. Muốn giữ hiệu ứng chạy mượt mà không đăng ký kiểu, cách duy nhất còn lại là tự cập nhật giá trị mỗi khung hình bằng requestAnimationFrame trong JavaScript.

Nên chọn gauge nửa vòng (04) hay vòng tròn đầy đủ (03)?

Gauge nửa vòng đọc nhanh hơn khi chỉ có một chỉ số cần theo dõi, vì mắt quen với hình dáng đồng hồ tốc độ. Vòng tròn đầy đủ tiết kiệm không gian hơn và hợp khi cần phân biệt vùng an toàn với vùng cảnh báo bằng màu, như mức dùng tài nguyên hay dung lượng pin — không nên trộn cả hai kiểu trên cùng một màn hình.

Đổi giá trị theo dữ liệu thật từ server thì làm sao?

Chỉ cần gán số mới vào biến --percent qua inline style, không phải viết lại animation nào cả. Vì --percent đã đăng ký kiểu qua @property, thêm một dòng transition vào phần tử là giá trị cũ tự chuyển mượt sang giá trị mới; bản React trong zip nhận percent qua props rồi làm đúng việc đó.

Nhập mật khẩu file nén

Mật khẩu nằm ngay trong bài viết này. Đọc tới đó bạn sẽ thấy.