Vòng quay đang tải

Biểu tượng vòng quay tải (hoặc chỉ báo hoạt động) cho người dùng biết rằng một quy trình đang diễn ra khi không biết chính xác thời lượng. Không giống như vòng tròn tiến trình, vòng quay là "không xác định" và thường sử dụng ảnh động để báo hiệu hoạt động.

Hướng dẫn này triển khai một danh sách màu sắc bằng cách:

  • Sử dụng phần tử <progress> gốc làm nền tảng ngữ nghĩa. Bằng cách bỏ qua thuộc tính value, trình duyệt sẽ coi thuộc tính này là một thanh tiến trình không xác định, đảm bảo thông báo chính xác bằng các công nghệ hỗ trợ.
  • Tạo kiểu cho thành phần bằng conic-gradient() để tạo "vệt" hình ảnh và background-clip: border-area để làm rỗng tâm thành một vòng tròn.
  • Tạo ảnh động cho vòng quay một cách hiệu quả bằng cách sử dụng các phép biến đổi CSS và tuân thủ prefers-reduced-motion để đảm bảo trải nghiệm thoải mái cho tất cả người dùng.

Xem phần Vòng tròn tiến trình để xử lý các tác vụ xác định có thời lượng đã biết.

Triển khai

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

Sử dụng phần tử <progress> gốc làm cả nguồn đáng tin cậy về ngữ nghĩa và thành phần hình ảnh. Nếu không có thuộc tính value, thì thuộc tính này sẽ ngầm định là không xác định.

<progress aria-label="Loading" class="loading-spinner"></progress>

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 quay, trước tiên, hãy ẩn kiểu trình duyệt mặc định cho các thanh tiến trình không xác đị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 và đường mòn của vòng quay

Biểu tượng vòng quay sử dụng conic-gradient để tạo một đường mòn trực quan.

@property --arc-start {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}
@property --arc-end {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}
/* Use a custom property to conditionally include the dash animation */
@property --progress-dash-animation {
  syntax: "*";
  inherits: false;
  initial-value: , progress-dash 3s ease-in-out infinite;
}

progress.loading-spinner:indeterminate {
  --_from: calc(90deg + var(--arc-start, 0deg));
  --_to: calc(90deg + var(--arc-end, 158deg));
  --size: 40px;
  --thickness: 2px;
  --spinner-color: #3b82f6;
  --track-color: #e2e5e7;
  --spinner-duration: 1.5s;
  --_used-spinner-duration: var(--spinner-duration);
  --spinner-timing: linear;

  position: relative;
  width: var(--size);
  height: var(--size);
  border-radius: 50%;
  appearance: none;

  /* Create the fading trail with dynamic angles */
  background: conic-gradient(
      from var(--_from),
      var(--spinner-color) calc(var(--_to) - var(--_from)),
      transparent 0
    )
    var(--track-color);

  @supports (background-clip: border-area) {
    background-clip: border-area;
    border: var(--thickness) solid transparent;
    background-origin: border-box;
  }

  /* ... fallback for background-clip: border-area ... */

  /* The dash animation is only included if @property is supported */
  animation:
    progress-spin var(--_used-spinner-duration) linear infinite
    var(--progress-dash-animation, );
}

@keyframes progress-spin {
  to {
    rotate: 1turn;
  }
}

@keyframes progress-dash {
  from {
    --arc-start: 0deg;
    --arc-end: 3deg;
  }
  50% {
    --arc-start: 100deg;
    --arc-end: 358deg;
  }
  to {
    --arc-start: 360deg;
    --arc-end: 363deg;
  }
}

Tôn trọng lựa chọn ưu tiên về chuyển động

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 nhìn thấy các thành phần quay nhanh. Luôn tuân thủ truy vấn đa phương tiện prefers-reduced-motion. Đặt thuộc tính --_used-spinner-duration nội bộ để ghi đè giá trị --spinner-duration của người dùng.

@media (prefers-reduced-motion: reduce) {
  .loading-spinner {
    /* Slow down the animation significantly rather than stopping it entirely,
       so the user still knows that the process is active. */
    --_used-spinner-duration: 6s;
  }
}

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

Nếu @property được hỗ trợ, thì ảnh động nét đứt sẽ tự động được đưa vào thông qua initial-value của thuộc tính --progress-dash-animation. Trong các trình duyệt không hỗ trợ @property, việc đăng ký thuộc tính sẽ bị bỏ qua và ảnh động sẽ quay lại chế độ xoay đơn giản. Bạn không cần dùng JavaScript cho phương án dự phòng này.

Đối với các 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.

/* Fallback: use mask-image to create the ring */
@supports not (background-clip: border-area) {
  --clip-boundary: calc(100% - var(--thickness));
  mask-image: radial-gradient(
    farthest-side,
    transparent var(--clip-boundary),
    black var(--clip-boundary)
  );
  border: 0;
}