로드 표시줄 구성요소 빌드

<progress> 요소를 사용하여 색상 적응형 및 접근성 높은 로딩 바를 빌드하는 방법에 관한 기본 개요입니다.

이 게시물에서는 <progress> 요소를 사용하여 색상 적응형 및 접근성 높은 로드 막대를 빌드하는 방법에 관한 생각을 공유하고자 합니다. 데모를 사용해 보고 소스를 확인하세요.

Chrome에서 시연된 밝은 모드와 어두운 모드, 불확정, 증가, 완료

동영상을 선호하는 경우 이 게시물의 YouTube 버전을 참고하세요.

개요

<progress> 요소는 사용자에게 완료에 관한 시각적 및 청각적 피드백을 제공합니다. 이러한 시각적 피드백은 양식 진행, 다운로드 또는 업로드 정보 표시, 진행률을 알 수 없지만 작업이 여전히 활성 상태임을 표시하는 등의 시나리오에 유용합니다.

이 GUI 챌린지는 기존 HTML <progress> 요소와 함께 작동하여 접근성 노력을 절약했습니다. 색상과 레이아웃은 기본 제공 요소의 맞춤설정 한계를 뛰어넘어 구성요소를 현대화하고 디자인 시스템에 더 잘 맞도록 합니다.

각 브라우저의 밝은 탭과 어두운 탭에서 적응형 아이콘의 개요를 위에서 아래로 보여줍니다. Safari, Firefox, Chrome
Firefox, Safari, iOS Safari, Chrome, Android Chrome에서 밝은 스키마와 어두운 스키마로 표시된 데모

마크업

암시적 관계를 위해 명시적 관계 속성을 건너뛸 수 있도록 <progress> 요소를 <label>로 래핑했습니다. 또한 로드 상태의 영향을 받는 상위 요소에 라벨을 지정하여 스크린 리더 기술이 해당 정보를 사용자에게 다시 전달할 수 있도록 했습니다.

<progress></progress>

value가 없으면 요소의 진행률은 확정되지 않습니다. max 속성의 기본값은 1이므로 진행률은 0과 1 사이입니다. 예를 들어 max을 100으로 설정하면 범위가 0~100으로 설정됩니다. 0과 1의 한도 내에서 진행률 값을 0.5 또는 50%로 변환하기로 했습니다.

라벨로 래핑된 진행률

암시적 관계에서는 진행률 요소가 다음과 같이 라벨로 래핑됩니다.

<label>Loading progress<progress></progress></label>

데모에서는 스크린 리더 전용 라벨을 포함하도록 선택했습니다. 이렇게 하려면 라벨 텍스트를 <span>로 래핑하고 화면에서 효과적으로 사라지도록 몇 가지 스타일을 적용합니다.

<label>
  <span class="sr-only">Loading progress</span>
  <progress></progress>
</label>

WebAIM의 다음 CSS를 사용합니다.

.sr-only {
  clip: rect(1px, 1px, 1px, 1px);
  clip-path: inset(50%);
  height: 1px;
  width: 1px;
  margin: -1px;
  overflow: hidden;
  padding: 0;
  position: absolute;
}

화면 준비 전용 요소를 보여주는 devtools의 스크린샷

로드 진행률의 영향을 받는 영역

시력이 정상인 경우 진행률 표시기를 관련 요소 및 페이지 영역과 연결하기가 쉽지만 시각 장애가 있는 사용자의 경우 명확하지 않습니다. 로드가 완료되면 변경되는 최상위 요소에 aria-busy 속성을 할당하여 이 문제를 개선하세요. 또한 aria-describedby를 사용하여 진행률과 로드 영역 간의 관계를 나타냅니다.

<main id="loading-zone" aria-busy="true">
  …
  <progress aria-describedby="loading-zone"></progress>
</main>

JavaScript에서 작업 시작 시 aria-busy를 true로 전환하고 완료되면 false로 전환합니다.

Aria 속성 추가

<progress> 요소의 암시적 역할은 progressbar이지만, 암시적 역할이 없는 브라우저를 위해 명시적으로 만들었습니다. 또한 indeterminate 속성을 추가하여 요소를 알 수 없는 상태로 명시적으로 설정했습니다. 이는 요소에 value가 설정되지 않은 것을 관찰하는 것보다 더 명확합니다.

<label>
  Loading 
  <progress 
    indeterminate 
    role="progressbar" 
    aria-describedby="loading-zone"
    tabindex="-1"
  >unknown</progress>
</label>

tabindex="-1"를 사용하여 JavaScript에서 진행률 요소에 포커스를 맞출 수 있도록 합니다. 이는 스크린 리더 기술에서 중요합니다. 진행률이 변경될 때 진행률에 포커스를 주면 업데이트된 진행률이 얼마나 도달했는지 사용자에게 알리기 때문입니다.

스타일

진행률 요소는 스타일 지정과 관련하여 약간 까다롭습니다. 기본 제공 HTML 요소에는 선택하기 어려울 수 있는 특수한 숨겨진 부분이 있으며, 설정할 수 있는 속성 집합이 제한적인 경우가 많습니다.

레이아웃

레이아웃 스타일은 진행률 요소의 크기와 라벨 위치에 어느 정도 유연성을 허용하도록 설계되었습니다. 특별한 완료 상태가 추가되어 유용하지만 필수는 아닌 추가 시각적 단서가 될 수 있습니다.

<progress> 레이아웃

진행률 요소의 너비는 그대로 유지되므로 디자인에 필요한 공간에 따라 축소 및 확장될 수 있습니다. 기본 제공 스타일은 appearance 및 border를 none로 설정하여 삭제됩니다. 이는 각 브라우저에 요소에 대한 자체 스타일이 있으므로 브라우저 간에 요소를 정규화할 수 있도록 하기 위함입니다.

progress {
  --_track-size: min(10px, 1ex);
  --_radius: 1e3px;

  /*  reset  */
  appearance: none;
  border: none;

  position: relative;
  height: var(--_track-size);
  border-radius: var(--_radius);
  overflow: hidden;
}

_radius의 1e3px 값은 과학적 숫자 표기법을 사용하여 큰 숫자를 표현하므로 border-radius는 항상 반올림됩니다. 1000px와 동일합니다. 설정하고 잊어버릴 수 있을 만큼 큰 값을 사용하는 것이 목표이므로 이 값을 사용하고 싶습니다(1000px보다 짧게 작성할 수 있음). 필요한 경우 더 크게 만들기도 쉽습니다. 3을 4로 변경하면 1e4px는 10000px와 같습니다.

overflow: hidden가 사용되며 논란이 되는 스타일입니다. 이렇게 하면 border-radius 값을 트랙 및 트랙 채우기 요소에 전달할 필요가 없는 등 몇 가지 작업이 쉬워졌지만 진행률의 하위 요소가 요소 외부에 있을 수 없다는 의미이기도 했습니다. 이 맞춤 진행률 요소의 또 다른 반복은 overflow: hidden 없이 실행할 수 있으며 애니메이션이나 더 나은 완료 상태를 위한 기회를 열어줄 수 있습니다.

진행 완료

CSS 선택자는 최댓값을 값과 비교하여 일치하면 진행률이 완료되는 어려운 작업을 수행합니다. 완료되면 가상 요소가 생성되어 진행률 요소 끝에 추가되어 완료에 대한 멋진 추가 시각적 단서를 제공합니다.

progress:not([max])[value="1"]::before,
progress[max="100"][value="100"]::before {
  content: "✓";
  
  position: absolute;
  inset-block: 0;
  inset-inline: auto 0;
  display: flex;
  align-items: center;
  padding-inline-end: max(calc(var(--_track-size) / 4), 3px);

  color: white;
  font-size: calc(var(--_track-size) / 1.25);
}

로딩 바가 100% 에 도달하고 끝에 체크표시가 나타나는 스크린샷

색상

브라우저는 진행률 요소에 자체 색상을 가져오며 하나의 CSS 속성만으로 밝은 모드와 어두운 모드에 맞게 조정됩니다. 이는 일부 특수한 브라우저별 선택기를 기반으로 빌드할 수 있습니다.

밝은 브라우저 스타일과 어두운 브라우저 스타일

사이트에서 어둡고 밝은 적응형 <progress> 요소를 선택하려면 color-scheme만 있으면 됩니다.

progress {
  color-scheme: light dark;
}

단일 속성 진행률 채우기 색상

<progress> 요소에 색조를 적용하려면 accent-color를 사용합니다.

progress {
  accent-color: rebeccapurple;
}

accent-color에 따라 트랙 배경 색상이 밝은 색상에서 어두운 색상으로 변경됩니다. 브라우저가 적절한 대비를 보장합니다. 아주 좋습니다.

완전 맞춤 밝은 색상 및 어두운 색상

<progress> 요소에 두 개의 맞춤 속성을 설정합니다. 하나는 트랙 색상이고 다른 하나는 트랙 진행률 색상입니다. prefers-color-scheme 미디어 쿼리 내에서 트랙 및 트랙 진행률에 새 색상 값을 제공합니다.

progress {
  --_track: hsl(228 100% 90%);
  --_progress: hsl(228 100% 50%);
}

@media (prefers-color-scheme: dark) {
  progress {
    --_track: hsl(228 20% 30%);
    --_progress: hsl(228 100% 75%);
  }
}

포커스 스타일

앞서 프로그래밍 방식으로 포커스를 맞출 수 있도록 요소에 음수 탭 색인을 지정했습니다. :focus-visible를 사용하여 포커스를 맞춤설정하여 더 스마트한 포커스 링 스타일을 선택합니다. 이렇게 하면 마우스 클릭 및 포커스에는 포커스 링이 표시되지 않지만 키보드 클릭에는 표시됩니다. YouTube 동영상에서 이 내용을 더 자세히 다루고 있으므로 검토해 볼 가치가 있습니다.

progress:focus-visible {
  outline-color: var(--_progress);
  outline-offset: 5px;
}

포커스 링이 있는 로딩 바의 스크린샷 색상이 모두 일치합니다.

브라우저 간 맞춤 스타일

각 브라우저에서 노출하는 <progress> 요소의 부분을 선택하여 스타일을 맞춤설정합니다. 진행률 요소를 사용하는 것은 단일 태그이지만 CSS 유사 선택기를 통해 노출되는 몇 가지 하위 요소로 구성됩니다. 설정을 사용 설정하면 Chrome DevTools에 다음 요소가 표시됩니다.

  1. 페이지를 마우스 오른쪽 버튼으로 클릭하고 엘리먼트 검사를 선택하여 DevTools를 표시합니다.
  2. DevTools 창의 오른쪽 상단에 있는 설정 톱니바퀴를 클릭합니다.
  3. Elements 제목 아래에서 Show 사용자 에이전트 shadow DOM 체크박스를 찾아 사용 설정합니다.

DevTools에서 사용자 에이전트 shadow DOM 노출을 사용 설정하는 위치를 보여주는 스크린샷

Safari 및 Chromium 스타일

Safari 및 Chromium과 같은 WebKit 기반 브라우저는 ::-webkit-progress-bar 및 ::-webkit-progress-value를 노출하여 CSS의 하위 집합을 사용할 수 있도록 합니다. 지금은 앞서 만든 맞춤 속성을 사용하여 background-color를 설정합니다. 이 속성은 밝은 모드와 어두운 모드에 맞게 조정됩니다.

/*  Safari/Chromium  */
progress[value]::-webkit-progress-bar {
  background-color: var(--_track);
}

progress[value]::-webkit-progress-value {
  background-color: var(--_progress);
}

진행률 요소의 내부 요소를 보여주는 스크린샷

Firefox 스타일

Firefox는 <progress> 요소에만 ::-moz-progress-bar 의사 선택기를 노출합니다. 또한 트랙에 직접 색조를 적용할 수 없습니다.

/*  Firefox  */
progress[value]::-moz-progress-bar {
  background-color: var(--_progress);
}

Firefox의 스크린샷과 진행률 요소 부분을 찾을 수 있는 위치

Safari, iOS Safari, Firefox, Chrome, Android용 Chrome에서 모두 로드 바가 작동하는 것으로 표시되는 디버깅 코너의 스크린샷

Firefox에는 accent-color에서 설정된 트랙 색상이 있고 iOS Safari에는 연한 파란색 트랙이 있습니다. 어두운 모드에서도 마찬가지입니다. Firefox에는 어두운 트랙이 있지만 설정한 사용자 지정 색상은 없으며 Webkit 기반 브라우저에서 작동합니다.

애니메이션

브라우저에 내장된 의사 선택기를 사용할 때는 허용되는 CSS 속성이 제한되는 경우가 많습니다.

트랙 채우기 애니메이션

진행률 요소의 inline-size에 전환을 추가하면 Chromium에서는 작동하지만 Safari에서는 작동하지 않습니다. Firefox도 ::-moz-progress-bar에서 전환 속성을 사용하지 않습니다.

/*  Chromium Only 😢  */
progress[value]::-webkit-progress-value {
  background-color: var(--_progress);
  transition: inline-size .25s ease-out;
}

:indeterminate 상태 애니메이션

여기서는 애니메이션을 제공할 수 있도록 좀 더 창의적으로 작업합니다. Chromium의 가상 요소가 생성되고 세 브라우저 모두에서 앞뒤로 애니메이션되는 그라데이션이 적용됩니다.

맞춤 속성

맞춤 속성은 여러 용도로 사용할 수 있지만, 제가 가장 좋아하는 용도는 마법처럼 보이는 CSS 값에 이름을 지정하는 것입니다. 다음은 상당히 복잡한 linear-gradient이지만 이름은 멋집니다. 목적과 사용 사례를 명확하게 이해할 수 있습니다.

progress {
  --_indeterminate-track: linear-gradient(to right,
    var(--_track) 45%,
    var(--_progress) 0%,
    var(--_progress) 55%,
    var(--_track) 0%
  );
  --_indeterminate-track-size: 225% 100%;
  --_indeterminate-track-animation: progress-loading 2s infinite ease;
}

맞춤 속성은 브라우저별 선택기를 함께 그룹화할 수 없으므로 코드를 DRY로 유지하는 데도 도움이 됩니다.

키프레임

목표는 앞뒤로 움직이는 무한 애니메이션입니다. 시작 및 종료 키프레임은 CSS에서 설정됩니다. 50%의 중간 키프레임인 하나의 키프레임만 있으면 시작 위치로 계속해서 돌아가는 애니메이션을 만들 수 있습니다.

@keyframes progress-loading {
  50% {
    background-position: left; 
  }
}

각 브라우저 타겟팅

모든 브라우저에서 <progress> 요소 자체에 의사 요소를 만들거나 진행률 표시줄에 애니메이션을 적용할 수 있는 것은 아닙니다. 의사 요소보다 트랙 애니메이션을 지원하는 브라우저가 더 많으므로 의사 요소를 기본으로 업그레이드하고 막대 애니메이션으로 업그레이드합니다.

Chromium 의사 요소

Chromium은 요소를 가리는 위치와 함께 사용되는 의사 요소 ::after를 허용합니다. 불확정 맞춤 속성이 사용되며 앞뒤 애니메이션이 매우 잘 작동합니다.

progress:indeterminate::after {
  content: "";
  inset: 0;
  position: absolute;
  background: var(--_indeterminate-track);
  background-size: var(--_indeterminate-track-size);
  background-position: right; 
  animation: var(--_indeterminate-track-animation);
}
Safari 진행률 표시줄

Safari의 경우 맞춤 속성과 애니메이션이 의사 요소 진행률 표시줄에 적용됩니다.

progress:indeterminate::-webkit-progress-bar {
  background: var(--_indeterminate-track);
  background-size: var(--_indeterminate-track-size);
  background-position: right; 
  animation: var(--_indeterminate-track-animation);
}
Firefox 진행률 표시줄

Firefox의 경우 맞춤 속성과 애니메이션도 의사 요소 진행률 표시줄에 적용됩니다.

progress:indeterminate::-moz-progress-bar {
  background: var(--_indeterminate-track);
  background-size: var(--_indeterminate-track-size);
  background-position: right; 
  animation: var(--_indeterminate-track-animation);
}

JavaScript

JavaScript는 <progress> 요소에서 중요한 역할을 합니다. 요소로 전송되는 값을 제어하고 스크린 리더를 위해 문서에 충분한 정보가 있는지 확인합니다.

const state = {
  val: null
}

데모에서는 진행 상황을 제어하는 버튼을 제공합니다. 이 버튼은 state.val를 업데이트한 다음 DOM을 업데이트하는 함수를 호출합니다.

document.querySelector('#complete').addEventListener('click', e => {
  state.val = 1
  setProgress()
})

setProgress()

이 함수는 UI/UX 조정이 발생하는 위치입니다. setProgress() 함수를 만들어 시작합니다. state 객체, 진행률 요소, <main> 영역에 액세스할 수 있으므로 매개변수가 필요하지 않습니다.

const setProgress = () => {
  
}

<main> 영역에서 로드 상태 설정

진행률이 완료되었는지 여부에 따라 관련 <main> 요소를 aria-busy 속성으로 업데이트해야 합니다.

const setProgress = () => {
  zone.setAttribute('aria-busy', state.val < 1)
}

적재량을 알 수 없는 경우 속성 삭제

값을 알 수 없거나 설정되지 않은 경우(이 사용 사례에서는 null) value 및 aria-valuenow 속성을 삭제합니다. 이렇게 하면 <progress>이(가) 불확정 상태로 전환됩니다.

const setProgress = () => {
  zone.setAttribute('aria-busy', state.val < 1)

  if (state.val === null) {
    progress.removeAttribute('aria-valuenow')
    progress.removeAttribute('value')
    progress.focus()
    return
  }
}

JavaScript 십진수 수학 문제 해결

진행률 기본 최댓값인 1을 유지하기로 선택했으므로 데모 증가 및 감소 함수는 십진수 수학을 사용합니다. JavaScript를 비롯한 다른 언어는 이러한 작업에 항상 적합한 것은 아닙니다. 다음은 수학 결과에서 초과분을 자르는 roundDecimals() 함수입니다.

const roundDecimals = (val, places) =>
  +(Math.round(val + "e+" + places)  + "e-" + places)

값을 표시할 수 있고 읽기 쉽도록 반올림합니다.

const setProgress = () => {
  zone.setAttribute('aria-busy', state.val < 1)

  if (state.val === null) {
    progress.removeAttribute('aria-valuenow')
    progress.removeAttribute('value')
    progress.focus()
    return
  }

  const val = roundDecimals(state.val, 2)
  const valPercent = val * 100 + "%"
}

스크린 리더 및 브라우저 상태의 값 설정

이 값은 DOM의 세 위치에서 사용됩니다.

  1. <progress> 요소의 value 속성입니다.
  2. aria-valuenow 속성
  3. <progress> 내부 텍스트 콘텐츠입니다.
const setProgress = () => {
  zone.setAttribute('aria-busy', state.val < 1)

  if (state.val === null) {
    progress.removeAttribute('aria-valuenow')
    progress.removeAttribute('value')
    progress.focus()
    return
  }

  const val = roundDecimals(state.val, 2)
  const valPercent = val * 100 + "%"

  progress.value = val
  progress.setAttribute('aria-valuenow', valPercent)
  progress.innerText = valPercent
}

진행 상황에 포커스 주기

값이 업데이트되면 시력이 정상인 사용자는 진행률 변경사항을 볼 수 있지만 스크린 리더 사용자에게는 아직 변경사항이 공지되지 않습니다. <progress> 요소에 포커스를 맞추면 브라우저에서 업데이트를 알립니다.

const setProgress = () => {
  zone.setAttribute('aria-busy', state.val < 1)

  if (state.val === null) {
    progress.removeAttribute('aria-valuenow')
    progress.removeAttribute('value')
    progress.focus()
    return
  }

  const val = roundDecimals(state.val, 2)
  const valPercent = val * 100 + "%"

  progress.value = val
  progress.setAttribute('aria-valuenow', valPercent)
  progress.innerText = valPercent

  progress.focus()
}

사용자에게 로드 막대의 진행 상황을 읽어주는 Mac OS Voice Over 앱의 스크린샷

결론

이제 제가 어떻게 했는지 알았으니 여러분은 어떻게 하시겠어요? 🙂

다시 기회가 주어진다면 몇 가지 변경하고 싶은 사항이 있습니다. 현재 구성요소를 정리할 여지가 있고 <progress> 요소의 의사 클래스 스타일 제한 없이 구성요소를 빌드할 여지가 있다고 생각합니다. 살펴볼 만한 가치가 있습니다.

접근 방식을 다양화하고 웹에서 빌드하는 모든 방법을 알아보겠습니다.

데모를 만들고 트윗 링크를 보내주시면 아래의 커뮤니티 리믹스 섹션에 추가하겠습니다.

커뮤니티 리믹스