Vòng tròn tiến trình

Vòng tròn tiến trình (hoặc thanh tiến trình hình tròn) cung cấp phản hồi trực quan về trạng thái của một nhiệm vụ. Không giống như thanh tiến trình tuyến tính, hình tròn của thanh tiến trình này rất phù hợp cho trang tổng quan, thành phần thẻ hoặc bất kỳ nơi nào có không gian hạn chế.

Hướng dẫn này triển khai vòng tròn tiến trình bằng cách:

  • Sử dụng phần tử <progress> gốc làm nền tảng ngữ nghĩa để đảm bảo thành phần này có thể truy cập được cho người dùng trình đọc màn hình và bàn phím ngay từ đầu.
  • Tạo kiểu cho thành phần bằng conic-gradient() và mask-image, cho phép tạo một vòng tròn hoàn toàn thích ứng và có thể áp dụng giao diện mà không cần thao tác phức tạp với đường dẫn SVG.
  • Tận dụng Thuộc tính tuỳ chỉnh CSS và @property để cho phép chuyển đổi mượt mà, tăng tốc GPU cho quá trình lấp đầy tiến trình.

Phương pháp này được ưu tiên hơn các giải pháp chỉ dùng SVG vì nó sử dụng phần tử <progress> ngữ nghĩa thay vì ARIA, đồng thời dễ dàng tích hợp với bố cục, hệ thống thiết kế và kiểu chữ hiện có.

Xem phần Vòng quay đang tải để xử lý các trạng thái tải không xác định.

Triển khai

1. Markup (note: đây là tên ứng dụng)

Sử dụng một trình bao bọc để chứa cả vòng hình ảnh và nội dung trung tâm không bắt buộc. Phần tử <progress> vẫn là nguồn thông tin chính xác về ngữ nghĩa. Sử dụng lớp tiện ích để ẩn thanh tiến trình gốc một cách trực quan trong khi vẫn có thể truy cập được.

<div class="ring-wrapper">
  <progress value="75" max="100" aria-label="Task progress" class="progress-ring"></progress>
  <!-- Optional: Content to display in the center -->
  <div class="ring-content">
    75%
  </div>
</div>

2. Kiểu

Ẩn giao diện người dùng gốc

Để tạo kiểu cho phần tử <progress> dưới dạng vòng tròn tiến trình, trước tiên, hãy ẩn kiểu trình duyệt mặc định cho thanh tiến trình.

/* Hide native bars */
progress.loading-spinner:indeterminate::-webkit-progress-bar {
  display: none;
  background: none;
}
progress.loading-spinner:indeterminate::-webkit-progress-value {
  display: none;
  background: none;
}
progress.loading-spinner:indeterminate::-moz-progress-bar {
  display: none;
  background: none;
}
progress.loading-spinner:indeterminate::slider-fill {
  display: none;
  background: none;
}

Vùng chứa và vòng

Trình bao bọc cung cấp bối cảnh định vị. Phần tử <progress> xử lý dải chuyển màu trực quan.

.progress-ring-wrapper {
  position: relative;
  display: grid;
  place-items: center;
}

progress.progress-ring {
  --size: 150px;
  --thickness: 16px;
  --track-color: #f1f5f9;
  --fill-color: #3b82f6;
  --value: attr(value type(<number>));
  width: var(--size);
  height: var(--size);

  transition: --value 0.4s cubic-bezier(0.4, 0, 0.2, 1);
  border-radius: 50%;

  background: conic-gradient(
    var(--fill-color) calc(var(--value) * 1%),
    var(--track-color) 0
  );

  /* MANDATORY: Clip the background to the border-area. */
  background-clip: border-area;
  border: var(--thickness) solid transparent;
  background-origin: border-box;
}

.progress-ring-content {
  /* Positioned in the center of the wrapper */
  position: absolute;
}

Bạn cũng có thể sử dụng radial-gradient để tạo các nắp đầu tròn.

Bật hiệu ứng chuyển đổi mượt mà bằng @property

Để tạo ảnh động cho vòng tròn tiến trình một cách mượt mà khi giá trị thay đổi, hãy đăng ký --value làm thuộc tính tuỳ chỉnh dạng số.

Người dùng nhạy cảm với chuyển động có thể cảm thấy mất phương hướng khi chuyển đổi giữa các giá trị. Tôn trọng truy vấn phương tiện prefers-reduced-motion bằng cách đặt thời lượng mặc định là 0 giây (ngay lập tức) và đặt thời gian dài hơn cho người dùng không có lựa chọn ưu tiên.

@property --value {
  syntax: '<number>';
  inherits: true;
  initial-value: 0;
}

progress.progress-ring {
  transition: --value 0s ease-in-out;
  @media (prefers-reduced-motion: no-preference) {
    transition-duration: 0.4s;
  }
}

3. Thông tin cập nhật về tiến trình

Cập nhật thuộc tính value trên phần tử <progress> và nội dung văn bản của .progress-ring-content (nếu hiển thị tỷ lệ phần trăm) bất cứ khi nào giá trị thay đổi.

4. Trạng thái thành công (không bắt buộc)

Bạn có thể sử dụng bộ chọn thuộc tính CSS để tự động cập nhật giao diện của vòng tròn (ví dụ: đổi màu thành màu xanh lục) khi nhiệm vụ hoàn tất.

/* Change the fill color to green when the progress reaches 100% */
progress.progress-ring[value="100"] {
  --fill-color: #10b981;
}

Chiến lược dự phòng

Đừng thêm giá trị dự phòng vào bên trong phần tử <progress>. Công nghệ hỗ trợ không sử dụng thuộc tính này và tất cả trình duyệt hiện đại đều bỏ qua thuộc tính này.

Ảnh động dự phòng

Nếu @property không được hỗ trợ, vòng sẽ chuyển ngay sang giá trị mới thay vì chuyển đổi mượt mà. Trong hầu hết các trường hợp, điều này không có vấn đề gì và không làm hỏng bất kỳ chức năng nào.

Nếu quá trình chuyển đổi là thực sự cần thiết, bạn có thể kiểm tra khả năng hỗ trợ @property và sử dụng vòng lặp requestAnimationFrame() để nội suy --value trong các trình duyệt cũ.

Đối với những trình duyệt chưa hỗ trợ background-clip: border-area, hãy quay lại mask-image để làm rỗng phần giữa của phần tử <progress>.

@supports not (background-clip: border-area) {
  mask-image: radial-gradient(
    transparent calc(50% - var(--thickness)),
    black calc(50% - var(--thickness) + 0.5px)
  );
  border: 0;
}

Đối với những trình duyệt không hỗ trợ hàm CSS attr() cho bất kỳ thuộc tính nào, hãy sử dụng MutationObserver để tự động đồng bộ hoá thuộc tính value với thuộc tính tuỳ chỉnh --value.

if (!CSS.supports("width: attr(value type(<number>))")) {
  const observer = new MutationObserver(() => {
    progress.style.setProperty("--value", progress.getAttribute("value"));
  });
  observer.observe(progress, {
    attributes: true,
    attributeFilter: ["value"],
  });
}