로딩 스피너

로딩 스피너 (또는 활동 표시기)는 정확한 소요 시간을 알 수 없을 때 프로세스가 진행 중임을 사용자에게 알려줍니다. 달성률 링과 달리 스피너는 '불확정'이며 일반적으로 애니메이션을 사용하여 활동을 알립니다.

이 가이드에서는 다음을 통해 스피너를 구현합니다.

  • 네이티브 <progress> 요소를 시맨틱 기반으로 사용합니다. value 속성을 생략하면 브라우저가 이를 불확정 진행률 표시줄로 처리하여 보조 기술에서 올바르게 읽을 수 있습니다.
  • conic-gradient()로 구성요소의 스타일을 지정하여 시각적 '트레일'을 만들고 background-clip: border-area로 중심을 비워 고리를 만듭니다.
  • CSS 변환을 사용하여 스피너에 효율적으로 애니메이션을 적용하고 prefers-reduced-motion를 준수하여 모든 사용자에게 편안한 환경을 제공합니다.

기간이 알려진 확정 작업을 처리하려면 진행률 링을 참고하세요.

구현

1. 마크업

네이티브 <progress> 요소를 시맨틱 정보 소스와 시각적 구성요소로 모두 사용합니다. value 속성이 없으면 암시적으로 불확정 상태입니다.

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

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

스피너 링 및 트레일

스피너는 conic-gradient를 사용하여 시각적 트레일을 만듭니다.

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

모션 환경설정 존중

움직임에 민감한 사용자는 빠르게 회전하는 요소로 인해 방향 감각을 잃을 수 있습니다. 항상 prefers-reduced-motion 미디어 쿼리를 준수합니다. 사용자의 --spinner-duration 값을 재정의하도록 내부 --_used-spinner-duration 속성을 설정합니다.

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

대체 전략

@property가 지원되는 경우 대시 애니메이션은 --progress-dash-animation 속성의 initial-value를 통해 자동으로 포함됩니다. @property를 지원하지 않는 브라우저에서는 속성 등록이 무시되고 애니메이션이 단순 회전으로 대체됩니다. 이 대체에는 자바스크립트가 필요하지 않습니다.

아직 background-clip: border-area를 지원하지 않는 브라우저의 경우 mask-image로 대체하여 가운데를 비웁니다.

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