建立載入列元件

基礎概念簡介:如何使用 <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.5 或 50%,維持在 0 和 1 的限制內。

標籤包裝進度

在隱含關係中,進度元素會由標籤包裝,如下所示:

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

螢幕截圖:開發人員工具顯示「screen ready only」元素。

受載入進度影響的區域

如果視力正常,您可能很容易將進度指標與相關元素和網頁區域建立關聯,但對視障使用者來說,這並不容易。如要改善這點,請將 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> 版面配置

進度元素的寬度不會變動,因此可隨著設計中需要的空間縮放。只要將 appearanceborder 設為 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;
}

_radius1e3px 值會使用科學記號表示大數,因此 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 開發人員工具就會向您顯示這些元素:

  1. 在網頁上按一下滑鼠右鍵,然後選取「檢查元素」,即可開啟開發人員工具。
  2. 按一下開發人員工具視窗右上角的「設定齒輪」圖示。
  3. 在「元素」標題下方,找出並啟用「顯示使用者代理程式陰影 DOM」核取方塊。

螢幕截圖:在開發人員工具中啟用公開使用者代理程式陰影 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 的螢幕截圖,顯示進度元素各部分的所在位置。

螢幕截圖:顯示「Debugging Corner」(偵錯專區),其中 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)
}

如果載入金額不明,請清除屬性

如果值不明或未設定,請移除 valuearia-valuenow 屬性。null這會將 <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 旁白應用程式向使用者朗讀載入列的進度。

結論

現在您已瞭解我的做法,您會怎麼做呢?🙂

如果還有機會,我一定會做出一些改變。我認為目前元件有改善空間,而且可以嘗試建構元件,避免 <progress> 元素虛擬類別樣式限制。值得探索!

讓我們多元化地運用各種方法,學習在網路上建構內容。

建立試聽版,然後在推特上傳送連結給我,我會將連結加到下方的社群混音區!

社群重混作品