Okrąg postępu

Okrąg postępu (lub okrągły pasek postępu) wizualnie informuje o stanie zadania. W przeciwieństwie do liniowego paska postępu jego okrągły kształt idealnie nadaje się do paneli, komponentów kart lub innych miejsc, w których przestrzeń jest ograniczona.

W tym przewodniku implementujemy okrąg postępu przez:

  • Użycie natywnego elementu <progress> jako podstawy semantycznej, aby zapewnić dostępność komponentu dla czytników ekranu i użytkowników klawiatury.
  • Stylizowanie komponentu za pomocą conic-gradient() i mask-image, co pozwala na uzyskanie w pełni responsywnego pierścienia z możliwością dostosowania motywu bez konieczności skomplikowanego manipulowania ścieżką SVG.
  • Wykorzystanie właściwości niestandardowych CSS i @property, aby umożliwić płynne, przyspieszane przez GPU przejścia wypełnienia paska postępu.

To podejście jest preferowane w porównaniu z rozwiązaniami opartymi wyłącznie na SVG, ponieważ używa semantycznego elementu <progress> zamiast ARIA i łatwiej integruje się z istniejącym układem, systemami projektowania i typografią.

Informacje o obsłudze nieokreślonych stanów wczytywania znajdziesz w sekcji Wskaźnik postępu wczytywania.

Implementacja

1. Znacznik

Użyj kontenera, aby umieścić w nim zarówno pierścień wizualny, jak i opcjonalną treść w środku. Element <progress> pozostaje semantycznym źródłem informacji. Użyj klasy narzędziowej, aby wizualnie ukryć natywny pasek postępu, zachowując jego dostępność.

<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. Style

Ukrywanie natywnego interfejsu użytkownika

Aby nadać elementowi <progress> styl okręgu postępu, najpierw ukryj domyślny styl przeglądarki dla pasków postępu.

/* 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;
}

Kontener i pierścień

Kontener zapewnia kontekst pozycjonowania. Element <progress> obsługuje gradient wizualny.

.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;
}

Możesz też użyć radial-gradient, aby utworzyć zaokrąglone zakończenia.

Włącz płynne przejścia za pomocą @property

Aby płynnie animować okrąg postępu, gdy wartość się zmienia, zarejestruj --value jako numeryczną właściwość niestandardową.

Użytkownicy wrażliwi na ruch mogą odczuwać dezorientację podczas przechodzenia między wartościami. Uwzględniaj zapytanie o media prefers-reduced-motion, ustawiając domyślnie czas trwania na 0 sekund (natychmiast) i dłuższy czas dla użytkowników, którzy nie mają preferencji.

@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. Aktualizacje postępów

Zaktualizuj atrybut value w elemencie <progress> i zawartość tekstową elementu .progress-ring-content (jeśli wyświetlana jest wartość procentowa) za każdym razem, gdy wartość się zmieni.

4. Opcjonalny stan sukcesu

Możesz użyć selektora atrybutu CSS, aby automatycznie aktualizować wygląd pierścienia (np. zmieniać kolor na zielony) po zakończeniu zadania.

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

Strategie zastępcze

Nie dodawaj wartości zastępczej w elemencie <progress>. Nie jest on używany przez technologie wspomagające osoby z niepełnosprawnością i jest ignorowany przez wszystkie nowoczesne przeglądarki.

Animacja zastępcza

Jeśli @property nie jest obsługiwany, pierścień natychmiast przeskoczy do nowej wartości, zamiast płynnie się zmieniać. W większości przypadków jest to prawidłowe i nie powoduje problemów z funkcjonalnością.

Jeśli przejście jest absolutnie konieczne, możesz sprawdzić obsługę @property i użyć pętli requestAnimationFrame(), aby interpolować --value w starszych przeglądarkach.

W przypadku przeglądarek, które nie obsługują jeszcze background-clip: border-area, użyj mask-image, aby wyciąć środek elementu <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;
}

W przypadku przeglądarek, które nie obsługują funkcji CSS attr() dla żadnej właściwości, użyj MutationObserver, aby automatycznie synchronizować atrybut value z właściwością niestandardową --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"],
  });
}