构建加载条组件

介绍如何使用 <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%。

标签封装进度

在隐式关系中,进度元素由标签封装,如下所示:

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

焦点样式

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

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

带有聚焦环的加载栏的屏幕截图。颜色全部匹配。

跨浏览器自定义样式

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

  1. 右键点击您的页面,然后选择 Inspect Element 以调出开发者工具。
  2. 点击开发者工具窗口右上角的“设置”齿轮。
  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);
}

显示进度元素内部元素的屏幕截图。

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

自定义属性也有助于使代码保持干净状态,因为同样,我们无法将这些特定于浏览器的选择器组合在一起。

关键帧

目标是制作出一个来回的无限动画。开始和结束关键帧将在 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 上构建应用的方法。

创建一个演示,在 Twitter 微博中发送链接,然后我会将其添加到下面的“社区混剪”部分!

社区混剪作品