构建加载条组件

简要介绍了如何使用 <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.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;
}

显示“屏幕已准备就绪”元素的开发者工具的屏幕截图。

受加载进度影响的区域

如果您的视力正常,则可以轻松将进度指示器与相关元素和网页区域相关联,但对于视障用户来说,这并不那么清晰。通过将 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%);
  }
}

焦点样式

之前,我们为该元素指定了负的 Tab 键索引,以便通过程序化方式聚焦该元素。使用 :focus-visible 自定义焦点,选择启用更智能的焦点环样式。这样一来,鼠标点击和焦点不会显示焦点环,但键盘点击会显示。YouTube 视频对此进行了更深入的探讨,值得一看。

progress:focus-visible {
  outline-color: var(--_progress);
  outline-offset: 5px;
}

屏幕截图:加载进度条,周围有聚焦环。所有颜色均匹配。

跨浏览器的自定义样式

通过选择每个浏览器公开的 <progress> 元素的部分来自定义样式。进度元素是一个标记,但它由几个通过 CSS 伪选择器公开的子元素组成。如果您启用以下设置,Chrome 开发者工具将向您显示这些元素:

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

屏幕截图:显示了在开发者工具中启用公开用户代理影子 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 的屏幕截图,显示了进度元素各个部分的所在位置。

调试角截图,其中显示 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()

此函数用于编排界面/用户体验。首先,创建一个 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 Voice Over 应用向用户朗读加载条进度的屏幕截图。

总结

现在您已经知道我是如何做到的,那么您会怎么做呢?🙂

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

让我们丰富方法,了解在 Web 上构建的所有方式。

制作演示视频,通过 Twitter 向我发送链接,我会将其添加到下方的社区混音部分!

社区混音作品