构建加载进度条组件

关于如何使用 <progress> 元素构建颜色自适应且无障碍的加载进度条的基础概述。

在这篇文章中,我想分享关于如何使用 <progress> 元素构建颜色自适应且无障碍的加载进度条的想法。试用演示查看源代码

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

如果您喜欢视频,这里是这篇文章的 YouTube 版本

概述

<progress> 元素为用户提供关于完成情况的视觉和听觉反馈。这种视觉反馈对于以下场景非常重要:表单的进度、显示下载或上传信息,甚至显示进度量未知但工作仍在进行中。

GUI 挑战使用了现有的 HTML <progress> 元素,以节省一些无障碍功能方面的工作。颜色和布局突破了内置元素自定义的限制,使组件现代化,并使其更好地适应设计系统。

Light and dark tabs in each browser providing an 
    overview of the adaptive icon from top to bottom: 
    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;
}

Screenshot of the devtools revealing the screen ready only element.

受加载进度影响的区域

如果您视力健康,则可能很容易将进度指示器与相关元素和页面区域关联起来,但对于视力障碍用户来说,这并不那么清楚。通过将 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;
}

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

Screenshot of the loading bar at 100% and showing a checkmark at the end.

颜色

浏览器为进度元素带来了自己的颜色,并且只需一个 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;
}

Screenshot of the loading bar with a focus ring around it. Colors all match.

跨浏览器的自定义样式

通过选择每个浏览器公开的 <progress> 元素的各个部分来自定义样式。使用进度元素是一个简单的标签,但它由几个子元素组成,这些子元素通过 CSS 伪选择器公开。如果您启用设置,Chrome DevTools 会向您显示这些元素

  1. 右键单击您的页面,然后选择检查元素以打开 DevTools。
  2. 单击 DevTools 窗口右上角的设置齿轮。
  3. 元素标题下,找到并启用显示用户代理 Shadow DOM 复选框。

Screenshot of where in DevTools to enable exposing the 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);
}

Screenshot showing the inner elements of the progress element.

Firefox 样式

Firefox 仅在 <progress> 元素上公开 ::-moz-progress-bar 伪选择器。这也意味着我们无法直接为轨道着色。

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

Screenshot of Firefox and where to find the progress element parts.

Screenshot of the Debugging Corner where Safari, iOS Safari, 
  Firefox, Chrome and Chrome on Android all have the loading bar shown working.

请注意,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()

此函数是 UI/UX 编排发生的地方。首先创建一个 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()
}

Screenshot of the Mac OS Voice Over app 
  reading the progress of the loading bar to the user.

结论

现在您知道我是如何做到的了,您会怎么做呢‽ 🙂

如果再给我一次机会,我肯定会做一些更改。我认为有空间清理当前的组件,也有空间尝试构建一个没有 <progress> 元素的伪类样式限制的组件。值得探索!

让我们多样化我们的方法,学习在 Web 上构建的所有方法。

创建一个演示,在 Twitter 上向我发送链接,我会将其添加到下面的社区混音部分!

社区混音