被大家遗忘的 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>
元素上未显式设置color
或noshade
值,该元素的border-style
将会渲染为inset
,否则会渲染为solid
注意,上面描述是仅针对于 Chrome 客户端的渲染结果进行的描述,具体效果如下:
为此,为了满足所有浏览器下的渲染效果能更一致,不建议在 <hr>
标签上使用 color
、align
、noshade
和 size
来定义其渲染效果,而更趋向于使用 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 文档中能看到的大多数是像 div
和 span
这样的通用标签元素(无语义的标签元素)。同样的,在 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>
在显式设置 color
、noshade
和 size
属性时,都有着不同的样式效果。但往往这些效果是无法满足 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 应用。 最后,希望这篇文章对你有所帮助,如果你在这方面有更好的建议或经验,欢迎在下面的评论中与我们共享。