被大家遗忘的 hr 标签元素

发布于 大漠

我们在 UI 还原的过程中,难免会碰到水平或垂直方向的分隔线。在现代 Web 的开发中,大部分前端开发者首先的方案是使用非 <hr> 标签元素,通过 CSS 或 SVG 来制作分隔线,即使在使用 React 或 Vue 构建 Separator (或 Divier)组件,也是采用非 <hr> 标签。可以说,时至今日,<hr> 标签元素已被大部分开发者给遗忘了。既然如此,为什么我要用一篇文章的篇幅来聊 <hr> 元素呢?这主要是出于 Web 的可读性(无障碍)出发点。希望阅读文这篇文章之后,你会有一个更好的选择。

UI 中的分隔线

在 Web 中,分隔线又被称之为“分离器”,主要用于两个区块之间的分隔,让用户从视觉上就能立马知道,分隔线的上下(或左右)两部分内容是有所区别的。另外,根据 UI 美感的需求,特别是在当下,分隔线的 UI 效果是非常有个性的,比如:

甚至还有一些更具艺术效果的分隔线 UI:

上图的分隔线 UI 效果来自 @Sven 的《The Big Horizontal Line Archive - Download your <hr> line now!》一文。

或许正因为上图这样的分隔线的效果,开发者很多时候更多的是采用 <img>div 这样的 HTML 元素使用 background-image 来实现具有浓厚艺术氛围的分隔线 UI 效果。

不过,我们今天这篇文章主要目的并不是聊怎么使用 CSS 实现不同分隔线 UI 的效果,而更多的是 Web 的开发中,碰到分隔线的场景时,应该不应该使用 <hr> 元素。

换句话说,我们怎么让有障碍的用户(比如,视障群体)在访问 Web 页面时,碰到分隔线,能很好的告诉这些用户。简单地说,使用 <hr> 除了保持语义和可访问性之外,也能让其适应各种上下文。

HTML 中的 <hr> 元素

HTML 的 <hr> 标签元素最初所表示的含义是 段落元素之间的主题转换,例如,一个故事中的场景的改变,或一个章节的主题的改变。在 HTML 的早期版本中,它是一条水平线,即使是在现在,它在浏览器的渲染也是一条水平线,但目前被定义为语义上的,而不是表现层面上。

HTML 的 <hr> 元素可以使用以下几个属性来设置其最初的表现形式:

  • align :设置对齐方式,默认值为left
  • color :设置颜色
  • noshade :去除阴影
  • width :使用像素或者百分比设置宽度

默认情况之下,客户端(浏览器)会 <hr> 有一个默认渲染,比如说,就一个纯 <hr> 时,浏览器给其默认的样式如下:

/* Chrome 版本 89.0.4389.90(正式版本) (x86_64) macOS Big Sur 11.2.3版本的系统 */
hr {
    display: block;
    unicode-bidi: isolate;
    margin-block-start: 0.5em;
    margin-block-end: 0.5em;
    margin-inline-start: auto;
    margin-inline-end: auto;
    overflow: hidden;
    border-style: inset;
    border-width: 1px;
}

以下所示的都是 Chrome 89的渲染效果。

如果在 <hr> 上显式设置不同的属性值,渲染的结果:

  • 如果 <hr> 元素上显式设置了一个非负值的 size 属性,那么浏览器将使用解析值(size 的值)除以 2 作为元素上 height 的值,且 border-width 的值为 1px
  • 如果 <hr> 元素上未显式设置 size 属性,那么浏览器不会渲染 height 值,只会渲染 border-width 的值为 1px
  • 如果 <hr> 元素上未显式设置 colornoshade 值,该元素的border-style将会渲染为inset,否则会渲染为 solid

注意,上面描述是仅针对于 Chrome 客户端的渲染结果进行的描述,具体效果如下:

为此,为了满足所有浏览器下的渲染效果能更一致,不建议在 <hr> 标签上使用 coloralignnoshadesize 来定义其渲染效果,而更趋向于使用 CSS 来设置其样式风格,比如:

hr {
    color: gray;
    border-style: inset;
    border-width: 1px;
    margin-block-start: 0.5em;
    margin-inline-end: auto;
    margin-block-end: 0.5em;
    margin-inline-start: auto;
    overflow: hidden;
}

而且最好是重置默认 hr 的样式:

/* CSS Normalizing */
hr {
    box-sizing: content-box;
    height: 0;
    overflow: visible;
}

或者像下面这样重置 hr 的样式:

hr {
    background-color: currentColor;
    border: none 0;
    height: 1px;
    width: 100%;
    color: inherit;
    overflow: visible
}

这样一来,我们就可以使用 CSS 给 hr 添加不同的样式风格。这里暂且不表,后面会和大家一起聊聊,CSS 怎么来设置 hr 样式。

HTML 的语义化和 Web 可访问性

前面提到过,HTML 的 <hr> 元素不仅仅表述的是视觉上的水平分隔线。它是具有一定语义的,并在其周围内容的上下文中发挥着有意义的作用。

HTML 的 <hr> 元素表示段落级的主题分隔,例如故事中的场景变化,或参考书中某一章节的另一个主题的过渡。

简单地说,<hr> 元素具有 隐含的分隔符 的作用。

因此,<hr> 能被屏幕阅读器理解并朗读出来。比如说,<hr> 在 iOS(或macOS)上会被朗读成“分割线”:

如果是在 macOS的旁白(VoiceVoer)会朗读成“水平分离器”

<hr> 也会被屏幕阅读器这样的ATs技术模式显示为一条“水平线”,这种情况之下,CSS 通常会被剥离出来,而 HTML 语义会被屏幕阅读器决定其样式。

有趣的是,<hr> 绘制的直线具有一个隐含 role 角色,其值是 separator,正如前面所看到的,如果不使用其他 CSS 来调整其布局方向,它默认情况之下是水平的。但我们在实际使用的时候,除了会用到水平的分隔线之外,还有可能会在垂直的方向用一条直线来分隔内容。比如下面这个示例:

<!-- HTML -->
<section horizontal>
    <article>The Article Contents element</article>
    <hr />
    <article>The Article Contents element</article>
</section>

<section vertical>
    <article>The Article Contents element</article>
    <hr />
    <article>The Article Contents element</article>
</section>

虽然在第二个区块中,<hr> 在用户面前是以垂直线的方式展示,但对于依赖屏幕阅读器的用户而言,屏幕阅读器朗读出来的结果与水平方向展示的结果是一样的:

如果希望在屏幕阅读器上识别出水平和垂直之间的差异,可以在 <hr> 标签上显式设置 aria-orientation 属性:

<hr aria-orientation="vertical" />

这个时候,macOS的旁白会将其朗读成 “垂直分割器”。

如果使用原生的 <hr> 标签制作分割线,不需要在该标签上显式设置 role = separator, 原因前面也提到过了,<hr> 具有隐式的 role 属性,且值为 separator

另外,除了在文章分离中使用“分割线”之外,该规则还可以用于其他的场景,比如菜单的分组隔离:

HTML 的 <hr> 标签是有语义的标签,在一些ATs终端上(比如文章中提到的 iOS 或 macOS的 VoiceOver)会朗读出其实际语义。如果在 Web 中使用 <hr> 时也需要注意,要是使用 <hr> 构建的水平或垂直的分割线不需要具备任何语义的时候(不是用其来对内容进行语义划分),需要在 <hr> 标签上使用 aria-hidden="true" 来隐藏它,不让屏幕阅读器识别。

进一步了解 role="separator"

A11Y系列的《:WAI-ARIA初探》中和大家探讨过,WAI-ARIA 整个规范中有三个主要的特色:角义(Role)、状态(State)和 属性(Property)

有关于 WAI-ARIA 更多的介绍,还可以阅读:

HTML 中元素大多都能和 ARIA 中的角色(Role)对应起来,即有一个隐式的 role 角色值,比如我们这篇文章介绍的 <hr> 元素的隐藏 role 值是 separator,可以告诉屏幕阅读器这样的ATs技术是什么?

不过,现代 Web 开发过程中,很多开发者不再太注重语义化标签的使用,甚至在现代很多复杂的 Web 页面开发过程中,无法很好的使用语义化的 HTML 标签元素来构建 Web 文档。因此,在 Web 文档中能看到的大多数是像 divspan 这样的通用标签元素(无语义的标签元素)。同样的,在 Web 开发中,碰到水平或垂直分割线的时候,大多数会用 div 元素来构建。这样一来,希望让屏幕阅读器能很好的识别这是一个分离器的话,我们就需要在 div 元素上显式使用 role="separator" 来告诉屏幕阅读器:

<!-- HTML -->
<div role="separator"></div>

回到 ARIA 中的 separator (Role) 角色。它主要用于 分隔或区分内容的部分或菜单项的分组。常见的分隔符主要有两种类型:

  • 只提供可见边界的静态结构
  • 可聚焦的交互式控件(Widget),它是可以移动的

如果一个分隔符是不可聚焦的,那么它将作为一个静态结构元素显示给辅助技术,比如用于在视觉上划分菜单中的两组菜单项,或页面的两个部分之间的分隔(也就是前面提到的示例)。

开发者可以让一个分隔符变成可聚焦的,可以用来创建一个既能在两部分之间提供可见的边框,又能使用用户通过改变分隔符的位置来改变各部分大小。可变分隔符小组件可以在一个范围内连续移动,而固定分隔符小组件只支持两个分隔开的位置。通常,固定分隔符小组件用于在展开和折叠状态之间切换中的一个部分。

如果分隔符是可聚焦的,开发者必须将 aria-valuenow 的值设置为反映分隔符当前位置的数字,并在其变化时更新该值。如果 aria-valuemin 的值不是 0 ,开发者还应该提供它的值;如果 aria-valuemax 的值不是 100,开发者还需提供它的值。如果缺失或不是一个数字,这些属性的隐式值如下:

  • aria -valuemin 的隐含值是 0
  • aria-valuemax 的隐式值是 100
  • aria-valuenow 的隐式值是 50

不过,我们在 Web 中常见到的分隔符,更多的是倾向于不可聚焦的分隔符。为此,如果不使用 <hr> 标签元素,而使用类似 div 标签,同时为了让屏幕阅读器能识别,需要添加一些 ARIA 相关的属性。

<!-- 水平分隔符,aria-orientation 默认为 horizontal -->
<div role="separator"></div>

<!-- 水平分隔符 -->
<div role="separator" aria-orientation="horizontal"></div>

<!-- 垂直分隔符 -->
<div role="separator" aria-orientation="vertical"></div>

美化 <hr> 风格

前面的内容简单地提到过,<hr> 在不同的客户端(浏览器)有着自己独特的样式风格,而且 <hr> 在显式设置 colornoshadesize 属性时,都有着不同的样式效果。但往往这些效果是无法满足 UI 的美观。为此,Web 开发者更喜欢使用 CSS 给 <hr> 设置不同的样式风格。比如,可以使用 CSS 来改为其宽度(width)、高度(height)、边框(border)和 颜色(background-color),甚至还可以使用 CSS 的渐变或背景图等美化其 UI 效果。尽管 <hr> 是一个无内容的元素,不过我们也可以使用伪元素(::before::after<hr> 创建样式:

除了使用 CSS 的样式来美化 <hr> 之外,还可以使用背景图片,特别是 SVG ,比如@LeaVerou 博客中的小鸟分隔线,用的就是 SVG:

可以将上面的 SVG 转换成 Base64的图像,并且运用到 hr

hr {
    background-image: url("data:image/svg+xml,...");
    background-repeat: no-repeat;
    background-size: 100% auto;
}

至于 SVG 转 Base64 的工具有很多,我个人喜欢 Iconset

也可以使用 svg-url-loader

不过使用 SVG 转换成 Base64 可以减少图像文件的请求数,特别是在请求不同风格的图像时。但是使用 SVG 转换的图像且要在 CSS 中改变和控制其颜色是有一定难度的。不过我们利用 CSS 自定义属性,可以让事情变得稍微简单一点。先在 SVG 的 <path> 元素上使用内联的 style 设置 fill的值,并且用 CSS 自定义属性来设置其值:

<?xml version='1.0' encoding='UTF-8'?><svg width='794px' height='51px' viewBox='0 0 794 51' version='1.1'
    xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'>
    <defs>
        <polygon id='path-1' points='0.907103825 0 798.907104 0 798.907104 364 0.907103825 364'></polygon>
    </defs>
    <g id='Page-1' stroke='none' stroke-width='1' fill='none' fill-rule='evenodd'>
        <g id='36257-O0KTX6' transform='translate(0.000000, -212.000000)'>
            <g id='Group' transform='translate(-4.907104, 171.000000)'>
                <g id='Group-70'>
                    <mask id='mask-2' fill='white'>
                        <use xlink:href='#path-1'></use>
                    </mask>
                    <g id='Clip-67'></g>
                    <path
                        d='M610.1... '
                        id='Fill-68' fill='black' style='fill: var(--hr-color, black)' mask='url(#mask-2)'></path>
                </g>
                <path
                    d='M204.6901...'
                    id='Fill-73' fill='black' style='fill: var(--hr-color, black)'></path>
                <path
                    d='M237.461104...'
                    id='Fill-79' fill='black' style='fill: var(--hr-color, black)'></path>
                <path
                    d='M490.3290... '
                    id='Fill-87' fill='black' style='fill: var(--hr-color, black)'></path>
                <path
                    d='M177.69... 18'
                    id='Fill-90' fill='black' style='fill: var(--hr-color, black)'></path>
                <path
                    d='M422.710... '
                    id='Fill-98' fill='black' style='fill: var(--hr-color, black)'></path>
                <path
                    d='M302.84510... 52.885'
                    id='Fill-99' fill='black' style='fill: var(--hr-color, black)'></path>
                <path
                    d='M542.099104 ... 39'
                    id='Fill-100' fill='black' style='fill: var(--hr-color, black)'></path>
            </g>
        </g>
    </g>
</svg>

我们可以动态修改 --hr-color 的值:

colorHandler.addEventListener("input", (etv) => {
    SvgElement.style.setProperty("--hr-color", etv.target.value);
});

尝试着调整示例中 --hr-color 的值,你将看到小鸟的颜色也会相应改变:

原本我想按着这样的思路,将带有内联 style="fill: var(--hr-color, black)" 的 SVG 转换成 Base64 放在 <hr>background-image 中,再通过 style.setProperty() 动态调整 --hr-color 的值:

事实证明,这个思路是行不通的,--hr-color 的值改变,但 background-image 中的引入的 Base64 中数据中的 style="fill: var(--hr-color, back)" 值虽会改变,但效果并不会变:

这个示例再次证明了,<hr> 元素自身是无法提供更多灵活性和真正自适应性的,即使是使用 SVG 转换出来的 Base64 文件。

内联 SVG 和 ARIA 结合实现灵活性,适应性强的分隔线

上面的示例帮我们验证了两点:

  • 内联的 SVG 灵活性,自适应性非常强
  • <hr> 元素自身没有内容,无法嵌套其他的 HTML 标签元素,并且使用 SVG 转换出来的 Base64 无法和内联的 SVG 等同

基于这两点,我们可以使用其他的方法来绕开 <hr> 无法内嵌 HTML 的限制,又要使用内联 SVG,并且同时保持有语义化能力(屏幕阅读器能识别出其是分隔符)。这个方法就是 将 SVG 内联到其他的 HTML 元素中,并且在该元素中使用描述 <hr> 语义的 ARIA 属性。比如:

<div class="separator" role="separator">
    <svg aria-hidden="true" role="img" width='794px' height='51px' viewBox='0 0 794 51'>
        <path ... />
    </svg>
</div>

这个时候,屏幕阅读器也能识别:

在 SVG 内联的情况下,我们可以使用 CSS 自定义属性来控制 SVG 的效果,比如控制小鸟分隔线的颜色:

svg path {
    fill: var(--hr-color, black);
}

const rootEle = document.documentElement;
const colorHandler = document.getElementById("color");

colorHandler.addEventListener("input", (etv) => {
    rootEle.style.setProperty("--hr-color", etv.target.value);
});

使用这种技术,我们还可以给 SVG 不同的元素使用不同的颜色,构建一个彩色的分隔线,同样拿上面的小鸟为例:

如果是直接使用内联的 SVG,我们可以直接在 <svg> 上使用 role="separator",减少 HTML 元素的嵌套使用:

<svg class="separator" role="separator" width='794px' height='51px' viewBox='0 0 794 51'>
    <path ... />
</svg>

如果分离器在你的 Web 构建中很常见的话,还可以将其封装成一个组件,比如 Material UI 中的 Divider 分隔线组件

小结

文章中主要和大家一起聊了聊 HTML 中的 <hr> 元素以及如何使用 CSS、SVG 和 ARIA等技术来构建屏幕阅读器能识别且 UI 又能足的分隔线。正如文章中所述,<hr> 标签元素是一个具有语义化的标签,且有一个隐式的 role="separator"。虽然使用 CSS 能构建出一些具有个性化的分隔线效果,但灵活性和可扩展性相对于使用内联 SVG 还是有一定的局限制。另外就是,如果使用非 <hr> 标签元素来构建分离器,希望能让ATs技术(屏幕阅读器)识别,需要显式使用 role="separator" 来增强其语义化。这样做的好处是,我们可以打破 <hr> 标签带来的局限性,换句话说,可以构建出更灵活,更具自适应的分隔线效果。不过有一点需要注意的话,如果你在构建 Web 的时候使用了 <hr> 来制作分隔线,但又不希望它具有任何语义化功能,那就需要显式的在 <hr> 标签上使用 aria-hidden="true" 让 ATs 技术不去识别。

时至今日,可能你不太关注 Web 语义化标签的具体使用了,但如果你希望构建一个更具有访问性的 Web 应用,这方面的知识还是不可或缺的。因为这些细节能帮助你构建更具可访问性的 Web 应用。 最后,希望这篇文章对你有所帮助,如果你在这方面有更好的建议或经验,欢迎在下面的评论中与我们共享。