构建加载条组件

基础概述了如何使用 <progress> 元素构建颜色自适应且可访问的加载栏。

在这篇博文中,我想分享一些想法,让您了解如何使用 <progress> 元素构建自适应且可访问的加载栏。试用演示版查看源代码

在 Chrome 上演示的浅色和深色、不确定、渐增和完成。

如果你更喜欢视频,可以参考本博文的 YouTube 版本:

概览

<progress> 元素可向用户提供有关完成情况的视觉和听觉反馈。这种视觉反馈对于如下场景很有用:表单显示进度、显示下载或上传信息,甚至显示进度量未知但工作仍处于活跃状态。

GUI 挑战采用现有的 HTML <progress> 元素,以减少无障碍功能方面的工作。颜色和布局突破了内置元素的自定义极限,使组件更具现代感,并使其更适合设计系统。

每个浏览器中的浅色标签页和深色标签页从上到下提供自适应图标的概览:Safari、Firefox、Chrome。
以浅色和深色方案在 Firefox、Safari、iOS Safari、Chrome 和 Android Chrome 中显示的演示。

Markup

我选择了将 <progress> 元素封装在 <label> 中,这样我可以跳过显式关系属性,改为使用隐式关系。我还标记了受加载状态影响的父元素,以便屏幕阅读器技术将这些信息传回用户。

<progress></progress>

如果没有 value,则元素的进度不确定max 属性默认为 1,因此进度介于 0 和 1 之间。例如,如果将 max 设置为 100,则会将范围设置为 0-100。我选择不超出 0 和 1 的限制,将进度值转换为 0.5 或 50%。

标签封装进度

在隐式关系中,Progress 元素由如下标签封装:

<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 设置为可聚焦。这对于屏幕阅读器技术非常重要,因为当进度发生变化时,如果指定进度焦点,系统会通知用户已更新进度的进度。

风格

在样式设置方面,Progress 元素有点棘手。内置 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% 并在末尾显示对勾标记的屏幕截图。

颜色

浏览器会为 progress 元素使用自有颜色,并且只需一个 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> 元素部分,自定义样式。使用 progress 元素作为单个标记,但它是由通过 CSS 伪选择器公开的几个子元素组成。启用此设置后,Chrome 开发者工具将向您显示这些元素:

  1. 右键点击您的网页,然后选择检查元素以调出开发者工具。
  2. 点击开发者工具窗口右上角的“Settings”齿轮。
  3. 元素标题下,找到并选中显示用户代理 shadow DOM 复选框。

在开发者工具中支持显示用户代理 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);
}

显示 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 上使用 transition 属性。

/*  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,请移除 valuearia-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 旁白应用的屏幕截图,该应用向用户显示加载栏的进度。

总结

现在你已经知道我是怎么做的,希望你怎么办 ‽ 🙂?

如果下次再有,我当然希望做出一些更改。我认为当前的组件还有改进空间,可以尝试构建一个没有 <progress> 元素的伪类样式限制的空间。值得一探!

让我们来了解一下我们采用的方法多样化,并了解在 Web 上构建网站的所有方法。

只需创建一个演示,点击 tweet me 链接,我就会将其添加到下方的“社区混剪”部分中!

社区混剪作品