달성률 링

달성률 링 (또는 원형 진행률 표시줄)은 작업 상태에 관한 시각적 피드백을 제공합니다. 선형 진행률 표시줄과 달리 원형 모양은 대시보드, 카드 구성요소 또는 공간이 제한된 모든 위치에 적합합니다.

이 가이드에서는 다음을 통해 달성률 링을 구현합니다.

  • 기본 <progress> 요소를 시맨틱 기반으로 사용하여 스크린 리더 및 키보드 사용자가 구성요소에 즉시 액세스할 수 있도록 합니다.
  • conic-gradient() 및 mask-image를 사용하여 구성요소의 스타일을 지정하면 SVG 경로 조작의 복잡성 없이 완전히 반응형이고 테마를 지정할 수 있는 링을 만들 수 있습니다.
  • CSS 맞춤 속성과 @property를 활용하여 진행률 채우기의 부드러운 GPU 가속 전환을 지원합니다.

이 접근 방식은 ARIA 대신 시맨틱 <progress> 요소를 사용하고 기존 레이아웃, 디자인 시스템, 서체와 더 쉽게 통합되므로 SVG 전용 솔루션보다 선호됩니다.

불확정 로드 상태를 처리하려면 로드 스피너를 참고하세요.

구현

1. 마크업

래퍼를 사용하여 시각적 링과 선택적 중앙 콘텐츠를 모두 보유합니다. <progress> 요소는 시맨틱 정보 소스로 유지됩니다. 유틸리티 클래스를 사용하여 네이티브 진행률 표시줄을 시각적으로 숨기면서도 접근성을 유지합니다.

<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. 스타일

네이티브 UI 숨기기

<progress> 요소를 달성률 링으로 스타일 지정하려면 먼저 진행률 표시줄의 기본 브라우저 스타일 지정을 숨깁니다.

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

컨테이너 및 링

래퍼는 위치 지정 컨텍스트를 제공합니다. <progress> 요소는 시각적 그라데이션을 처리합니다.

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

radial-gradient를 사용하여 둥근 엔드 캡을 만들 수도 있습니다.

@property로 원활한 전환 사용 설정

값이 변경될 때 달성률 링에 부드러운 애니메이션을 적용하려면 --value를 숫자 맞춤 속성으로 등록합니다.

움직임에 민감한 사용자는 값 간의 전환으로 인해 혼란을 느낄 수 있습니다. 기본적으로 0초 (즉시) 지속 시간을 설정하고 환경설정이 없는 사용자의 경우 더 긴 시간을 설정하여 prefers-reduced-motion 미디어 쿼리를 준수합니다.

@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. 진행 상황 업데이트

값이 변경될 때마다 <progress> 요소의 value 속성과 .progress-ring-content의 텍스트 콘텐츠(백분율을 표시하는 경우)를 업데이트합니다.

4. 선택적 성공 상태

CSS 속성 선택기를 사용하여 작업이 완료되면 링의 모양을 자동으로 업데이트할 수 있습니다 (예: 색상을 녹색으로 변경).

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

대체 전략

<progress> 요소 내에 대체 값을 추가하지 마세요. 보조 기술에서 사용되지 않으며 모든 최신 브라우저에서 무시됩니다.

애니메이션 대체

@property가 지원되지 않으면 링이 부드럽게 전환되지 않고 새 값으로 즉시 이동합니다. 대부분의 경우 이 방법은 문제가 없으며 기능을 중단하지 않습니다.

전환이 반드시 필요한 경우 @property 지원을 확인하고 requestAnimationFrame() 루프를 사용하여 이전 브라우저에서 --value를 보간할 수 있습니다.

아직 background-clip: border-area를 지원하지 않는 브라우저의 경우 mask-image로 대체하여 <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;
}

모든 속성에 대해 attr() CSS 함수를 지원하지 않는 브라우저의 경우 MutationObserver를 사용하여 value 속성을 --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"],
  });
}