基本概览:如何使用 <progress> 元素构建颜色自适应且符合无障碍标准的加载栏。
在本文中,我想分享关于如何使用 <progress> 元素构建颜色自适应且无障碍的加载栏的想法。试用演示版并查看源代码!
如果您更喜欢视频,可以观看此博文的 YouTube 版本:
概览
<progress> 元素会向用户提供有关完成情况的视觉和听觉反馈。这种视觉反馈在以下场景中非常有用:表单填写进度、显示下载或上传信息,甚至在进度未知但工作仍在进行时显示。
此 GUI 挑战使用了现有的 HTML <progress> 元素,以节省无障碍功能方面的工作量。颜色和布局突破了内置元素的自定义限制,使组件更加现代化,并更好地融入设计体系。
Markup
我选择将 <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%);
}
}
聚焦样式
之前,我们为该元素指定了负 tab 索引,以便能够以编程方式聚焦。使用 :focus-visible 自定义焦点,以选择启用更智能的聚焦环样式。这样一来,鼠标点击和焦点不会显示焦点环,但键盘点击会显示。YouTube 视频对此进行了更深入的探讨,值得观看。
progress:focus-visible {
outline-color: var(--_progress);
outline-offset: 5px;
}

跨浏览器的自定义样式
通过选择每个浏览器公开的 <progress> 元素部分来自定义样式。使用 progress 元素是一个单一标记,但它由几个通过 CSS 伪选择器公开的子元素组成。如果您启用以下设置,Chrome 开发者工具会向您显示这些元素:
- 右键点击页面,然后选择检查元素以打开开发者工具。
- 点击 DevTools 窗口右上角的“设置”齿轮。
- 在 Elements 标题下,找到并选中 Show user agent 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()
此函数是界面/用户体验编排发生的位置。首先,创建一个 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> 元素伪类样式限制的组件。值得探索!
让我们采用多样化的方法,学习在 Web 上构建应用的所有方式。
创建演示,在 Twitter 上向我发送链接,我会将其添加到下面的社区混剪部分!