基本總覽:如何使用 <progress> 元素建構色彩自動調整且無障礙的載入列。
在本文中,我想分享如何使用 <progress> 元素,建構色彩自動調整且無障礙的載入列。試用示範並查看來源!
如果您比較喜歡看影片,可以觀看這篇貼文的 YouTube 版本:
總覽
<progress> 元素會向使用者提供視覺和聽覺回饋,告知使用者已完成。這類視覺回饋在多種情境下都很有用,例如表單填寫進度、顯示下載或上傳資訊,甚至在進度不明但工作仍在進行時,也能提供視覺回饋。
這項 GUI 挑戰使用現有的 HTML <progress> 元素,節省無障礙功能方面的作業。顏色和版面配置突破了內建元素的自訂限制,讓元件更具現代感,更符合設計系統。
標記
我選擇將 <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;
}

受載入進度影響的區域
如果視力正常,很容易就能將進度指標與相關元素和頁面區域連結,但視障使用者可能無法清楚辨識。如要改善這個問題,請將 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);
}

顏色
瀏覽器會為進度元素提供專屬顏色,並透過單一 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 開發人員工具就會顯示這些元素:
- 在網頁上按一下滑鼠右鍵,然後選取「檢查元素」,開啟開發人員工具。
- 按一下開發人員工具視窗右上角的「設定」齒輪圖示。
- 在「Elements」標題下方,找到並啟用「Show 使用者代理程式 shadow DOM」核取方塊。

Safari 和 Chromium 樣式
WebKit 瀏覽器 (例如 Safari 和 Chromium) 會公開 ::-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 的軌道顏色是從 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 的三個位置:
<progress>元素的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
}
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()
}

結論
現在你已經知道我的做法,你會怎麼做?🙂
如果能重來,我一定會做一些調整。我認為目前的元件還有改進空間,可以嘗試建構不含 <progress> 元素虛擬類別樣式限制的元件。值得一試!
讓我們嘗試各種方法,瞭解所有建構網頁的方式。
建立示範、推文給我連結,我會將其新增至下方的社群混音部分!