聊聊aria-label、aria-labelledby和aria-describedby
近一年来一直以来都在致力让手淘互动项目更有温度,虽然借助自己所掌握的有关于A11Y(Web可访问性)相关的理论知识,让互动项目更具可访问性,但其中有很多细节还是有待于完善,特别是焦点冗余部分,更是令我感到头痛。为了优化这方面的细节,我尝试着通过 WAI-ARIA 中 aria-label、 aria-labelledby 和 aria-describedby 属性来进行优化,却事与愿违,而且这几个属性一直令我感到困惑。为了彻底的能搞清楚这几个属性,我打算花一些时间来和大家一起探讨它们。如果你对这几个属性感兴趣的话,请继续往下阅读。
先说一下背景
一直以来,在无障碍实施或优化方面都有一个令我感到非常头痛的问题(现象),那就是开启“旁白”朗读的时焦点过于太细化,也就是焦点冗余。比如下图所示:

再把问题细化一下,卡片中的价格“¥109.00”我们在构建HTML的时候分面三个节点:
<div>
<span>¥</span>
<span>109</span>
<span>.00</span>
</div>
千万别问我为什么要这样来构建HTML的DOM结构,这是有一定的需求在这里。也正因为是这个原因,在部分屏幕阅读器中三个<span>都会有焦点:

屏幕阅读器会将其朗读:“日元符,109, .00”。使用浏览器开发者工具不能发现,它们对应着相应的可访问树:

起初我想着,借用aria-label、aria-labelledby和aria-describedby几个属性,事情应该不会过于复杂,比如:
<!-- aria-label -->
<div aria-label="¥109.00">
<span>¥</span>
<span>109</span>
<span>.00</span>
</div>
<!-- aria-labelledby -->
<div aria-labelledby="a11y__node--1 a11y__node--2 a11y__node--3">
<span id="a11y__node--1">¥</span>
<span id="a11y__node--2">109</span>
<span id="a11y__node--3">.00</span>
</div>
<!-- aria-describedby -->
<div aria-describedby="a11y__node--1 a11y__node--2 a11y__node--3">
<span id="a11y__node--1">¥</span>
<span id="a11y__node--2">109</span>
<span id="a11y__node--3">.00</span>
</div>
最终结果,并没有达到预期所要的结果。
那这是为什么呢?为了找到相应的答案或者搞清楚为什么没有达到预期结果,我重新查阅了 WAI-ARIA相关的文档。从规范中得知,“可访问名称和描述有着映射关系”:
Information concerning name and description accessibility API mappings, including relationships, such as labelled-by/label-for and described-by/description-for, is documented in the Core Accessibility API Mappings specification [CORE-AAM-1.1]. See the mapping table entries for
aria-label,aria-labelledby, andaria-describedby.
大致意思是:核心易访问性API映射规范中记录了关于可访问性名称和描述对应的映射关系,包括关系,比如标签由(labelled-by)、标签为(label-for)和描述由(describled-by)、描述为(description-for)。比如ARIA中的aria-label、aria-labelledby和aria-describedby等属性。
可访问性的几个重要概念
在详细介绍或探索aria-label、aria-labelledby和aria-describedby之前,有几个重要的概念必须先理清楚:
ARIA标签和关系
先来了解一下ARIA标签属性和关系属性。
ARIA标签属性
在ARIA中提供了多种向元素添加标签和说明的机制。事实上,ARIA是唯一一种可以添加可访问帮助或说明文本的方式。
而aria-label、aria-labelledby、aria-describedby这几个就是ARIA中用于创建可访问标签的属性。
ARIA关系属性
无论页面元素的DOM属性如何,关系属性都会在它们之间创建语义关系。比如aria-labelledby,关系将是“此元素由另一个元素标记”。
ARIA规范中有八个关系属性,除了我们将要探索的aria-describedby、aria-labelledby之外,还有aria-activedescendant、aria-controls、aria-flowto、aria-owns、aria-posinset和aria-setsize。其中除了aria-flowto和aria-posinset之外的六个关系属性都是通过引用一个或多个元素的方式在页面元素之间创建一个新链接。各个属性的区别是链接的含义及向用户呈现的方式。
可访问名称和描述
每个平台访问性API都包含一种方法,用于为在可访问性树中创建的每个可访问对象分配和检索可访问名称和可访问描述属性。这些相关属性的实现方式和名称因API的不同而有所不同。
例如,在MSAA中,所有可访问对象都支持accName属性,该属性存储对象的可访问名称。如果对象还支持具有可访问的描述,则MSAA将此避税性存储在对象的accDescription属性中。
使用ATK的软件可以读写对象的可访问名称(accessible-name)和可访问描述(accessible-description)属性。反过来,AT-SPI可以通过它的atspi_accessible_get_name和atspi_accessible_get_description函数查询这些属性的值。
UIA可访问性树中的自动化元素有一个Name属性。当对象还支持具有可访问的描述时,UIA将此属性存储在对象的FullDescription属性中。
在AX API中实现可访问名称和可访问描述的方法与其他平台API有些不同。当名称被可视化呈现时,可访问名称使用AXTitle属性公开,而当对象的名称未被可视化呈现时,使用AXDescription属性。对象的可访问性描述(如果提供的话)应该始终在AXHelp属性中公开。
其中ATK与实现者最相关,而AT-SPI与消费者最相关。在映射WAI-ARIA的角色(roles)、状态(states)和属性(properties)的上下文中,用户代理是实现者并使用ATK。ATs技术(屏幕阅读器)是消费者,使用AT-SPI。
其中对MSAA、ATK、AT-SPI、UIA、AX API等专业术语不太了解,不要过于紧张,因为这些在我们接下来的内容关连性不会很紧密。不过对于“可访问名称”和“可访问描述”两个概念需要有一定的了解。
可访问名称
可访问名称(Accessible Name)是用户界面元素的名称。每个平台可访问性API都提供了可访问名称(accessible-name)属性。可访问名称的值可能来自用户界面元素的可见属性(比如按钮上可见的文本)或不可见属性(如描述图标的可替代文本,即alt中的值)。
来看一个说明可访问名称属性简单用法的示例,比如有一个“OK”按钮。文本“OK”是可访问名称。当按钮获得焦点时,辅助技术(比如屏幕阅读器)可以将平台的角色描述与可访问名称连接起来。例如,屏幕阅读器可能会朗读“按钮 OK”或“OK 按钮”。角色描述的连接顺序和细节(例如,“按钮”、“按钮”、“可点击的按钮”)由平台可访问性API或辅助技术决定。
可访问描述
可访问描述提供了与接口元素相关的附加信息,补充了可访问名称。可访问性描述可能是可视的,也可能不是。
可访问树(AOM)和 DOM树
可访问性树(AOM)和DOM树是并行结构。粗略地说,可访问性树是DOM树的一个子集。它包括用户代理的用户界面对象和文档的对象。在可访问性树中为应该公开给辅助技术的每个DOM元素创建可访问对象,因为它可能触发可访问性事件,或者因为它有需要公开的属性、关系或特性。一般来说,出于性能或简单性的考虑,如果某些内容可以删除,那么它就会删除。例如,只有 现代战争样式更改而没有语义的<span>可能无法获得它自己的可访问对象,但是样式更改将通过其他方式公开。
或者说AOM和DOM之间的关系,用下图来描述可能会更易于理解一些:

对于用户界面和辅助技术,也可以用AOM和DOM来描述它们之间的关系:

注意,可访问树AOM在A11Y中是一个非常重要的知识体系,如果要彻底的掌握或更好的构建可访问性Web应用,有必要掌握AOM相关的知识。但在这里将不做过多阐述,如果你对这方面知识感兴趣的话,建议你花时间阅读下面这些教程:
- Accessibility Object Model
- Accessibility Object Model
- Today, I learned about the Accessibility Tree
- Web Components and the Accessibility Object model (AOM)
- The Accessibility Tree
- Playing with the Accessibility Object Model (AOM)
aria-label、aria-labelledby和aria-describedby
有了上面的基础我们来看aria-label、aria-labelledby和aria-describedby的具体使用。
aria-label、aria-labelledby和aria-describedby是ARIA的属性,它们主要是给HTML元素添加可访问性名称。
在构建Web页面的时候,并不是所有元素都具备可访问性名称,如果需要为更多不同用户群体访问Web提供平等权利以及更好的体验,就需要借助ARIA相关的特性为其提供可访问性名称和可访问性描述。比如说,在Web上有某种指明元素用途的视觉指示(例如,使用图形而不是文本的按钮),但是仍需要向无法获取视觉指示(例如,仅使用图像指示其用途的按钮)的任何人阐明该用途。

这个时候,我们就需要aria-label、aria-labelledby和aria-describedby等属性为图标按钮提供相应的描述。虽然他们三个都可以给元素提供可访问性名称和描述(或者关联描述),但他们的使用还是有所差异的,而且不同的ATs技术对其呈现方式也会有所差异。在接下来的示例主要以iOS的“VoiceOver”来做测试。
aria-label
我们从最基本的开始。
众所周知,在构建Web应用或Web页面时,很多元素(HTML标签)都会有自己文本内容或者说可描述的内容,比如说:
- 标题
<h1></h1>会有自己的文本内容 - 图像
<img>会有可替代文本(alt) - 交互元素,比如
<input>会有<label>相关联
对于有可访问性描述,屏幕阅读器一般情况之下都可以很好的向用户呈现,比如:
<h1>欢迎来到W3cplus!</h1>
当标题<h1>获得焦点时,“VoiceOver”会朗读:
欢迎来到W3cplus!,标题级别1

如果我们把<h1>换成无语义的<div>标签,这个时候“VoiceOver”会朗读:
欢迎来到W3cplus!

使用Chrome浏览器开发者工具查看“Accessibility”选项,可以看到<h1>和<div>对应的AOM:

上面两个示例,不管是有语义的标签元素(<h1>)还是无语义的标签元素(<div>)都有对应的文本内容,“VoiceOver”都可以将其文本节点朗读出来。接下来,我们来看一个没有文本的示例:
<button>
<svg t="1602668295844" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4611" width="200" height="200">
<path d="M669.781333 130.752c71.637333-11.093333...z" fill="#3D3D3D" p-id="4612"></path>
</svg>
</button>
<div>
<svg t="1602668350662" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4741" width="200" height="200">
<path d="M662.634667 353.749333c33.493333-33.301333 ...z" fill="#3D3D3D" p-id="4742"></path>
</svg>
</div>
当焦点在<button>元素上时,“VoiceOver”会告诉你:
按钮
可能是,心形
但并不知道是什么“按钮”,对于一些视力有障碍的人群,他们就无法获取到按钮上的信息:

而该示例中的<div>无法获得焦点,“VoiceOver”不会有任何信息的输出。
针对于这样的场景,我们就可以使用aria-label属性。
<button aria-label="喜欢">
<svg t="1602668295844" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4611" width="200" height="200">
<path d="M669.781333 130.752c71.637333-11.093333...z" fill="#3D3D3D" p-id="4612"></path>
</svg>
</button>
<div aria-label="礼包">
<svg t="1602668350662" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4741" width="200" height="200">
<path d="M662.634667 353.749333c33.493333-33.301333 ...z" fill="#3D3D3D" p-id="4742"></path>
</svg>
</div>
此时,<button>得到焦点时,“VoiceOver”会告诉用户:
喜欢,按钮

对于<div>元素而言,即使显式的给元素添加了aria-label="礼包",“VoiceOver”同样是无法获取到相应的焦点,也不会向用户朗读出aria-label指定的值。致于为什么?暂且不做阐述,先来看看两者的AOM上的差异:

接着再尝试一下,在<div>这个非聚集元素上显示的使用role添加一个角色,比如role="link":
<div aria-label="礼包" role="link">
<svg t="1602668350662" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4741" width="200" height="200">
<path d="M662.634667 353.749333c33.493333-33.3013...z" fill="#3D3D3D" p-id="4742"></path>
</svg>
</div>
这个时候,“VoiceOver”可以在<div>元素上获取焦点,并且会将aria-label的值朗读出来:
礼包,链接

在AOM中也可以发现,它新增了link角色:

从这个示例中我们可以初步的获知:
只有运用于可聚集元素或者说有角色的元素上,
aria-label才会被ATs技术识别;另外aria-label的值在非ATs技术(非屏幕阅读器)是不会向用户呈现的,即,只有开启了屏幕阅读器的用户才能获得aria-label的值。
我们再来看另外一种情况,aria-label和元素文本内容同在的情景:
<button aria-label="喜欢">我喜欢你</button>
<div aria-label="跳到W3cplus" role="link">W3cplus</div>
<h1 aria-label="欢迎来到W3cplus">你来吧,我等你!</h1>
就上面的示例,“VoiceOver”在对应的焦点元素上会向用户呈现:
喜欢,按钮
跳到W3cplus,链接
欢迎来到W3cplus,标题级别1
不难发现,aria-label的值替代了对应元素的“文本内容”:

从AOM中我们可以得知,aria-label的值将会覆盖元素自己的文本内容:

就<button>和<h1>元素而言,它们有着默认的role角色,而且像<button>这样的元素还是自带可聚集的元素,而<div>是人为的通过role="link"改变了其默认的角色,如果说,在<div>元素,显式的设置aria-label并且不显式设置role的值,会是什么样的情形呢?
在上面的示例中,把<div>的role去掉:
<div aria-label="跳到W3cplus">W3cplus</div>
这个时候,对于<div>元素,“VoiceOver”只会将<div>自身的文本内容向用户呈现,对于aria-label将会忽略不计:
W3cplus

也就是说:
对于可聚焦元素或有
role角色的元素,如果显式设置了aria-label属性,那么aria-label的值将会替代元素自身的文本内容;对于没有role角色的元素,即使显式设置了aria-label也将不会起任何作用,最终呈现给用户的是元素自身的文本内容。
回到我们最初的示例场景:
<div>
<span>¥</span>
<span>109</span>
<span>.00</span>
</div>
即使我们在<div>元素上显式设置aria-label="¥109.00"对于“VoiceOver”也将是不起任何作用的。正如前面示例所示,<div>元素既不是可聚焦元素,也没有 role角色。如果希望让“VoiceOver”能比较友好的向用户呈现“¥109.00”,而不是分多个焦点呈现,我们需要在<div>元素上显示设置role的值。那么问题来了,在WAI-ARIA中role关近70个,我们应该使用哪一个角色更为合理呢?比如,document、group、application、option还是region,其实我也不知道。既然如此,我们分别来看一下他们之间的差异:
<div aria-label="¥109.00" role="document">
<span aria-hidden="true">¥</span>
<span aria-hidden="true">109</span>
<span aria-hidden="true">.00</span>
</div>
<div aria-label="¥109.00" role="group">
<span aria-hidden="true">¥</span>
<span aria-hidden="true">109</span>
<span aria-hidden="true">.00</span>
</div>
<div aria-label="¥109.00" role="application">
<span aria-hidden="true">¥</span>
<span aria-hidden="true">109</span>
<span aria-hidden="true">.00</span>
</div>
<div aria-label="¥109.00" role="option">
<span aria-hidden="true">¥</span>
<span aria-hidden="true">109</span>
<span aria-hidden="true">.00</span>
</div>
<div aria-label="¥109.00" role="region">
<span aria-hidden="true">¥</span>
<span aria-hidden="true">109</span>
<span aria-hidden="true">.00</span>
</div>
从实际的验证效果,我们不难发现role="option"可以得到焦点,并且可以将相关信息呈现给用户:

似乎和我们所需要的诉求很接近了。对于其他的几个,在iOS的Safari中无法获得焦点,即使改变tabindex的值也是如此。
暂且使用
role="option"和aria-label来满足我们减少分段得到焦点的诉求。
那么哪些元素可以和aria-label一起使用:
- 交互元素:例如带有
href的<a>,当<controls>出现的<audio>、<video>,<input>,<select>,<button>,<textarea> - 隐含的地标元素:
<header>,<footer>,<nav>,<main>,<aside>,<section>,<form> - 显示设置地标的元素:显式设置了
role的值为header,navigation,main,banner,complementary,contentinfo,search,form和application - 设置控件的
role:比如role="dialog",role="tooltip",在ARIA中一共有27个控件相关的role iframe和img元素
aria-labelledby
aria-labelledby允许我们将DOM中另一个元素的id指定为当前元素的标签。

这非常类似于使用label元素,但也存在一些关键区别:
aria-labelledby可以用于任何元素,而不仅仅是可标记元素label元素引用其标记的对象,但对于aria-labelledby来说,关系则相反 —— 被标记的对象引用标记它的元素- 只有一个标签元素与可标记元素关联,但是
aria-labelledby可以利用一组IDREF从多个元素构建标签。标签将按照IDREF的提供顺序串联 - 可以使用
aria-labelledby引用隐藏和不在可访问性树中的元素。例如,你可以在想要标记的元素旁添加一个隐藏的span,然后使用aria-labelledby引用该元素 - 由于ARIA仅影响可访问性树,
aria-labelledby并不会展现使用label元素时熟悉的标签点击行为
简单地来说,aria-labelledby是一个关系属性示例。无论页面元素的DOM属性如何,关系属性都会在它们之间创建语义关系。可能这样描述不易于理解。我们先从HTML的<label>标签开始。在我们构建表单时,时常将带有for属性的<label>标签和对应id的表单控件绑定在一起,比如:
<form>
<label for="user">用户名:</label>
<input id="user" type="text" name="user" placeholder="请输入用户名" />
</form>
开启屏幕阅读器时,<input>将会获得焦点,“VoiceOver”会朗读:
用户名:,请输入用户名,文本栏,轻点两下来编译。

如果我们在上面的Demo上稍作改良,即在<input>上添加aria-labelledby:
<form>
<label for="user">用户名:</label>
<input id="user" name="user" placeholder="请输入用户名" aria-labelledby="input__des" />
<span id="input__des" hidden>用户名只能是英文或汉语拼音</span>
</form>
注意,<input>上aria-labelledby的值和<span>上的id相匹配,就好比<input>上的id和<label>上的for相匹配。这个时候我们开启“VoiceOver”,它将呈现给用户的是:
用户名:,请输入用户名,文本栏,轻点两下来编译。
从这个示例我们可以获知:
在表单元素上,如果
<label>和aria-labelledby同时有表单控件有绑定关系,那么label的优先级高于aria-labelledby。
如果把<label>标签注释掉:
<form>
<div id="login">登录表单</div>
<!-- <label for="user">用户名:</label> -->
<input id="user" type="text" name="user" placeholder="请输入用户名" aria-labelledby="login" />
</form>
“VoiceOver”实测的结果:
登录表单,请输入用户名,文本栏,轻点两下来编辑

注意,在 HTML to Platform Accessibility APIs Implementation Guide 对表单控件和标签关系有着明确的说明,这里不做过多阐述。
我们还是回到文章开头的示例中来:
<div>
<span>¥</span>
<span>109</span>
<span>.00</span>
</div>
原本我期望,在父容器<div>使用aria-labelledby来绑定<span>中的id,即可达到期望的效果:
<div aria-labelledby="a11y__span--1 a11y__span--2 a11y__span--3">
<span id="a11y__span--1">¥</span>
<span id="a11y__span--2">109</span>
<span id="a11y__span--3">.00</span>
</div>
但事与愿围,“VoiceOver”还是分段向用户呈现:
¥
109
.00

根据aria-label的使用经验,我在<div>上显示添加role="option"角色:
<div aria-labelledby="a11y__span--1 a11y__span--2 a11y__span--3" role="option">
<span id="a11y__span--1">¥</span>
<span id="a11y__span--2">109</span>
<span id="a11y__span--3">.00</span>
</div>
似乎是我想要的结果,“VoiceOver”呈现给用户的是:
¥109.00

再一起来看另外的场景,先来看可聚集的元素(自带role角色的元素),比如button和h1:
<h1 aria-labelledby="heading">W3cplus</h1>
<button aria-labelledby="button">点击我</button>
<div id="heading">我就是喜欢W3cplus</div>
<div id="button">前往W3cplus站点查看内容</div>
当<h1>和<button>分别获得焦点时,“VoiceOver”呈现给用户的是:
我就是喜欢W3cplus,标题级别1
前往W3cplus站点查看内容,按钮

不难发现:
可聚焦元素(指的是自带
role角色的元素),如果显式的设置aria-labelledby的话,那么和aria-labelledby相关的描述文本将会替代元素自身的文本内容。
针对这一现象,我们通过AOM可以看得更清楚:

在该示例上做进一步的优化,我们在div#heading和div#button上分别通过HTML的hidden和CSS的sr-only将内容隐藏让用户不可见。因为这部分内容主要是用来给屏幕阅读器呈现的。
<!-- HTML -->
<h1 aria-labelledby="heading">W3cplus</h1>
<button aria-labelledby="button">点击我</button>
<div id="heading" hidden>我就是喜欢W3cplus</div>
<div id="button" class="sr-only">前往W3cplus站点查看内容</div>
/* CSS */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
clip-path: inset(100%);
white-space: nowrap;
border-width: 0;
}
最终效果和前面是一样的:
我就是喜欢W3cplus,标题级别1
前往W3cplus站点查看内容,按钮
甚至是将div#heading和div#button设置为display: none,对于屏幕阅读器依旧有同样的效果。
aria-labelledby和aria-label有一个明显的差异,aria-labelledby可以利用一组 IDREF 从多个元素构建标签,即 aria-labelledby可以同时绑定多个id不同的标签元素,而且读屏器呈现给用户的是按照绑定id的前后顺序(IDREF顺序)输出。比如下面这个示例:
<button aria-labelledby="a11y__id--1 a11y__id--2 a11y__id--3">WoW!</button>
<button aria-labelledby="a11y__id--3 a11y__id--2 a11y__id--1">MoM!</button>
<span hidden id="a11y__id--1">走一走,</span>
<span hidden id="a11y__id--2">还是</span>
<span hidden id="a11y__id--3">停一停</span>
“WoW!”和“MoM!”两个按钮分别获得焦点时,“VoiceOver”向用户呈现的结果并不一样:
走一走,还是 停一停,按钮
停一停 还是 走一走,,按钮

在实际使用的时候,aria-label和aria-labelledby可以同时运用于同一个元素上,比如:
<button aria-label="喜欢" aria-labelledby="des">WoW</button>
<div id="des" hidden>我喜欢这篇文章</div>
“VoiceOver”将会忽略<button>上的aria-label,只会朗读aria-labelledby绑定的id对应的文本内容:
我喜欢这篇文章,按钮

从AOM中我们可以发现,aria-labelledby会覆盖aria-label以及元素自身的文本内容:

这个示例告诉我们:
当
aria-labelledby和aria-label都被使用时,用户代码在生成可访问的名称属性时将为aria-labelledby分配更高有优先级,即aria-labelledby会覆盖aria-label。注意,由于不同的辅助技术对于这一技术的处理可能不同,可能在不同的ATs技术上aria-labelledby不一定会覆盖aria-label。
既然在一些ATs技术上aria-label和aria-labelledby只能二选一,我们在实际使用的时候在选哪个时可以根据下图做选择:

即:
- 需要使用ARIA来增加Web可访问性?
- 如果需要使用ARIA,那么文本是否已经存在HTML文档中?
- 如果文本存在于HTML文档中,则使用
aria-labelledby;如果没有,则使用aria-label
第一条也是ARIA使用的第一条规则:
如果可以使用具有所需的语义和行为的原生HTML元素或属性,则不要在HTML元素上添加ARIA的角色,状态或属性来增强Web可访问性
有一些地方可以在不使用aria-label或aria-labelledby的情况下为元素提供可访问的名称。例如在<a>或<button>直接放置文本内容:
<a href="https://www.fedev.cn">W3cplus</a>
<button>喜欢</button>
比如在<img>中直接使用alt来描述图像:
<img src="path/logo.png" alt="欢迎来到W3cplus" />
比如使用<label>和 <input>、<select>、<textarea>等表单控件绑定在一起:
<div class="control">
<label for="user">用户名:</label>
<input type="text" id="user" name="user" placeholder="大漠" />
</div>
但有的时候是无法直接使用具有语义的标签元素和属性。当然也有的时候,开发者普通使用没有语义的标签,比如<div>和<span>,特别是在移动端的开发。那么这个时候就需要使用ARIA来给Web做可访问性。那么就会涉及aria-label和aria-labelledby的选择,这个时候就可以按上图来做选择。
比如前面的示例所示,一个图标按钮,也没有任何文本存在于文档中,那么就可以使用aria-label:
<button aria-label="喜欢">
<svg t="1602668295844" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4611" width="200" height="200">
<path d="M669.781333 130.752c71.637333-11.093333...z" fill="#3D3D3D" p-id="4612"></path>
</svg>
</button>
比如在开发Web时,会使用图像替代文本,这个时候也可以使用aria-label,比如:
<!-- HTML -->
<h1 aria-label="欢迎来到W3cplus"></h1>
/* CSS */
h1 {
background: url('path/logo.png') no-repeat center;
}
但有的时候,文本内容本来就存在于HTML文档中。比如前面的示例:
<div>
<span>¥</span>
<span>109</span>
<span>.00</span>
</div>
这个时候我们采用aria-labelledby更为合理:
<div role="option" aria-labelledby="a11y__span--1 a11y__span--2 a11y__span-3">
<span id="a11y__span--1">¥</span>
<span id="a11y__span--2">109</span>
<span id="a11y__span--3">.00</span>
</div>
也就是说,当文本内容在屏幕上可见时,更应该选择
aria-labelledby,而不应该选择aria-label。
另外,我们在构建一些交互控件时,更应该使用aria-labelledby,比如我们熟悉的 Modal、Tooltips、Alert等:
<!-- Modal -->
<div class="modal fade" id="exampleModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Modal title</h5>
<button type="button" class="close" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
...
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary">Close</button>
<button type="button" class="btn btn-primary">Save changes</button>
</div>
</div>
</div>
</div>
<!-- Tooltips -->
<button class="notifications" aria-labelledby="notifications-label">
<svg><use xlink:href="#notifications-icon"></use></svg>
</button>
<div role="tooltip" id="notifications-label">Notifications</div>
<!-- Popup Menu -->
<div role="menubar">
<div role="menuitem" aria-haspopup="true" id="fileMenu">File</div>
<div role="menu" aria-labelledby="fileMenu">
<div role="menuitem">Open</div>
<div role="menuitem">Save</div>
<div role="menuitem">Save as ...</div>
</div>
</div>
aria-describedby
aria-describedby 提供了一种可访问说明,方式与 aria-labelledby 提供标签的方式相同。 与 aria-labelledby 一样,aria-describedby 可能引用不可见的元素,无论这些元素在 DOM 中隐藏,还是对辅助技术用户隐藏。如果存在用户可能需要的额外说明性文本,则不管该文本适用于辅助技术用户还是所有用户,这种技术都非常有用。
像aria-label和aria-labelledby两个标签一样,我们通过不同的示例来全面的了解aria-describedby。
先来看表单控件,在设计表单控件的时候,除了有 标签(<label>)、 控件(<input>)还会有 描述文本(或辅助文本) 来更好的告诉用户更好的完成表单的填写:

来看一个常见的示例是密码输入字段带有一些说明性文本,其中,说明性文本用于说明最低密码要求。 与标签不同,此说明不一定会呈现给用户;用户可以选择是否访问说明,此说明可能跟在其他信息之后,也可能被其他内容抢占。例如,如果用户正在输入信息,他们的输入将回显并且可能中断元素的说明。因此,说明是一种用于传达补充但非必要信息的绝佳方式;它不会妨碍更关键的信息,例如元素角色。
<form>
<div class="control">
<label for="password">密码:</label>
<input type="password" id="password" name="password" placeholder="输入密码" aria-describedby="password__describedby" />
<span id="password__describedby">输入的密码需要带有大小写英文字母,字符和数字</span>
</div>
</form>
<input>得到焦点时,“VoiceOver”会给用户输出:
密码:,输入密码,安全文本栏,输入的密码需要带有大小写英文字母,字符和数字

我们再来验证span#password__describedby是隐藏状态下效果:
<span id="password__describedby" hidden>输入的密码需要带有大小写英文字母,字符和数字</span>
在“VoiceOver”中效果是一样的。我们可以再来看看它的AOM:

同样来看看不可聚焦的元素:
<div aria-describedby="a11y__span--1 a11y__span--2 a11y__span--3" role="option">
<span id="a11y__span--1">¥</span>
<span id="a11y__span--2">109</span>
<span id="a11y__span--3">.00</span>
</div>
实测结果,在没有role="option"时,同样会分段获得焦点:

如果有role="option"时,div自身会有焦点:

相就的AOM如下图所示:

同样的,aria-describedby除了可以应用在表单控件元素上之外,还可以运用在一些可交互的元素,和具有语义的元素上,比如:
<h1 aria-describedby="heading">W3cplus</h1>
<button aria-describedby="button">
<svg t="1602749413134" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2330" width="200" height="200">
<path d="M608.929951 671.715102...z" p-id="2332"></path>
<path d="M639.892491 783.696288 639.89249...z" p-id="2333"></path>
</svg>
</button>
<div id="heading" hidden>欢迎来到W3cplus!</div>
<div id="button" hidden>客服</div>
上面的示例,“VoiceOver”会向下面这样向用户呈现:
W3cplus,标题级别1,欢迎来到W3cplus
客服,按钮

来看它们对应的AOM:

从这个示例中我们可以知道:
aria-describedby和aria-label、aria-labelledby有点不同,aria-label和aria-labelledby都将会覆盖元素自身的文本内容,但aria-describedby却不会覆盖。
比如下面这个示例:
<h1 aria-label="欢迎来到W3cplus">W3cplus</h1>
<h1 aria-labelledby="heading-labelledby">你喜欢?</h1>
<h1 aria-describedby="heading-describedby">欢迎来到W3cplus</h1>
<div id="heading-labelledby" hidden>欢迎来到W3cplus!</div>
<div id="heading-describedby" hidden>阅读你喜欢的文章</div>
示例中三个<h1>,分别使用aria-label、aria-labelledby和aria-describedby,就上例而言,“VoiceOver”给用户呈现的信息分别是:
欢迎来到W3cplus,标题级别1
欢迎来到W3cplus!,标题级别1
欢迎来到W3cplus,标题级别1,阅读你喜欢的文章

从它们的AOM中可以得知为什么为是这样的结果:

在介绍aria-labelledby时我们知道,当aria-labelledby和aria-label同时用来描述一个元素时,那么aria-labelledby的权重要高于aria-label(aria-labelledby会覆盖aria-label)。同样的,在aria-describedby和aria-labelledby以及aria-label也有可能同时给元素创建可访问性描述。那么它们之间的权重将会是什么?
先来看aria-describedby和aria-labelledby同时出现在一个元素上:
<h1 aria-labelledby="heading-labelledby" aria-describedby="heading-describedby">W3cplus</h1>
<div id="heading-labelledby" hidden>欢迎来到W3cplus!</div>
<div id="heading-describedby" hidden>阅读你喜欢的文章</div>
“VoiceOver”向用户呈现的是:
欢迎来到W3cplus!, 标题级别1,阅读你喜欢的文章

根据前面所掌握的知识,应该是这样的一个过程:
aria-labelledby绑定的id为heading-labelledby的文本内容将会替代<h1>自身文本节点的内容aria-describedby绑定的id为heading-describedby的文本内容不会替代<h1>自身文本节点的内容,但在部分屏幕阅读器中会在后面追加
针对于上面的示例,我们查看<h1>元素对应的AOM,可以了解的更清楚:

在上面的示例基础上,将aria-labelledby换成aria-label,并在aria-label上指定新的内容,比如“走吧,我们去吧”:
<h1 aria-label="走吧,我们去吧" aria-describedby="heading-describedby">W3cplus</h1>
<div id="heading-describedby" hidden>阅读你喜欢的文章</div>
“VoiceOver”呈现给用户的是:
走吧,我们去吧,标题级别1,阅读你喜欢的文章

从AOM输出的结果可以看到aria-label的内容替代了元素<h1>自身的文本内容:

接着看另一种情景,aria-label、aria-labelledby和aria-describedby都同时在<h1>出现:
<h1 aria-label="走吧,我们去吧" aria-labelledby="heading-labelledby" aria-describedby="heading-describedby">W3cplus</h1>
<div id="heading-labelledby" hidden>一直喜欢W3cplus</div>
<div id="heading-describedby" hidden>阅读你喜欢的文章</div>
“VoiceOver”呈现给用户的是:
一直喜欢W3cplus,标题级别1,阅读你喜欢的文章

不难发现,上面的示例中aria-label和<h1>元素自身文本都被忽略,屏幕阅读器输出的是aria-labelledby和aria-describedby绑定的id的元素内容。其实这也是正常的表现行为,在aria-label和aria-labelledby同时出现时,aria-labelledby权重要高于aria-label,而且aria-label和aria-labelledby都将覆盖元素<h1>自身的文本内容,但aria-descrbedby并不覆盖其他标签属性的值,也不会覆盖元素自身文本节点内容。同样,我们也可以从AOM中看出来:

还有,aria-describedby和aria-labelledby同样可以和一些组件结合在一起,比如:
<div id="dialog" role="dialog" hidden aria-labelledby="d_head" aria-describedby="instructions">
<h1 id="d_head">Enter code</h1>
<div id="instructions">
<p>The security number for your credit card was missing when checking out.</p>
<figure>
<img src="credit-card-security-location.jpg" alt="security code shown on the right back side of credit card."
<figcaption>Enter the number as shown.</figcaption>
</figure>
</div>
<button>OK</button>
</div>
最后再来看一个示例,就是aria-describedby同时绑定多个id:
<h1 aria-describedby="a11y__des--1 a11y__des--2 a11y__des--3">W3cplus</h1>
<h1 aria-describedby="a11y__des--3 a11y__des--2 a11y__des--1">WoW</h1>
<div id="a11y__des--1" hidden>Hi,</div>
<div id="a11y__des--2" hidden>我们一起去W3cplus</div>
<div id="a11y__des--3" hidden>阅读你喜欢的文章</div>
“VoiceOver”呈现给用户的是:
W3cplus,标题级别1,Hi,我们一起去W3cplus 阅读你喜欢的文章
WoW,标题级别1,阅读你喜欢的文章 我们一起去W3cplus Hi,

同样把AOM的表现形式截图展示:

就这一点,不难发现:
aria-describedby在绑定多个id时,它的表现形式和aria-labelledby同时绑定多个id相似,ATs也是按照IDREF顺序向用户呈现。
aria-label、aria-describedby和aria-labelledby简单小结
我们在实际中使用aria-label、aria-labeelledby和aria-describedby属性时还是要小心一些,因为它们在所有HTML元素中并不一致。虽然W3C规范中有说过,他们可以运用于所有的HTML元素上,但事实上能不能真正的被ATs技术识别还是有待商榷,就好上面文章中向大家展示的示例,像div这样无语义,不可聚焦,又没有任何ARIA角色的元素,即使显式使用了这几个标签,都不一定能识别,比如“VoiceOver”读屏器。
从上面的示例中,我们不难发现,aria-label、aria-labelledby和aria-describedby属性主要可以用于:
- 交互元素,比如带有
href的<a>,带有controls的<audio>和<video>,非hidden的<input>,<select>,<button>和<textarea> - 具有标记性角色的元素,比如
<header>,<footer>,<main>,<nav>,<aside>,<section>和<form> - 使用
role的控件,比如dialog,slider,progressbar和tooltip等,在ARIA规范中有具有role角色的控件有27个 - 显示设置地标的元素:显式设置了
role的值为header,navigation,main,banner,complementary,contentinfo,search,form和application iframe和img元素
如果你将aria-label,aria-labelledby或aria-describedby与任何其他元素(比如div,span,p,blockquote或strong等)一起使用,它们通常无法跨所有用户代理(比如浏览器,ATs技术)组合工作。
就同一个元素(也就是可以使用aria-label,aria-labelledby或aria-describedby属性的元素),其中:
aria-label和aria-labelledby将会替代元素自身的文本内容aria-label和aria-labelledby同时出现时,aria-labelledby权重要更高aria-describedby不会替代元素自身的文本内容,只是会在其基础上追加aria-labelledby和aria-describedby都可以同时绑定多个元素的id,而且屏幕阅读器朗读的内容顺序和绑定的id顺序有关
简单地说,ARIA中的
aria-label、aria-labelledby和aria-describedby在不同的使用场景,或者说运用于不同的role以及在不同的用户代理或ATs技术中,它们的表现形式都会有所差异。
aria-label、aria-labelledby和aria-describedby的使用实例
最后我们来看aria-label、aria-labelledby和aria-describedby的使用实例。先来看图标按钮,我们如何使用aria-label、aria-labelledby和aria-describedby来给图标按钮提供更好的可访问性。
到目前为止,使用SVG的图标按钮场景越来越多,而且在很多应用中开发者更偏向于使用<div>和<svg>构建图标按钮,比如:
<div class="button" tabindex="-1" role="button">
<svg t="1602911663963" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2330" width="200" height="200">
<path d="M982.028557 404.405174 573.32303 ...z" p-id="2331"></path>
</svg>
</div>
就上面的示例而言,很多ATs可并无法识别出它是一个什么样的“按钮”,比如“VoiceOver”向用户呈现的是:
按钮,它可能是,家
对于正常用户群体而言,他们可以通过图标知道这个按钮代表的真正含义,但对于一些视弱或有视觉障碍的用户群体而言,并无法准确的清楚这个按钮的真正含义。
为了给用户提供更好的体验,特别是有障碍的用户群体,开发者可能会像下面这样来使用:
<!-- HTML -->
<div class="button" tabindex="0" role="button">
<svg t="1602911663963" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2330" width="200" height="200">
<path d="M982.028557 404.405174 ...z" p-id="2331"></path>
</svg>
<span class="sr-only">首页</span>
</div>
/* CSS */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
clip-path: inset(100%);
white-space: nowrap;
border-width: 0;
}
屏幕阅读器会告诉用户,这个是一个首页按钮:

从AOM中可以看到<span class="sr-only">的文本内容构建了按钮的可读性名称以及可读性描述:

另外,图标本身(<svg>)可能和屏幕阅读器用户无关,它们它并不能很好的告诉用户(按钮的可访问名称)。因此,常见的做法是使用aria-hidden来向屏幕阅读器隐藏图标:
<div class="button" tabindex="0" role="button" aria-label="首页">
<svg t="1602911663963" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2330" width="200" height="200" aria-hidden="true" focusable="false">
<path d="M982.028557 404.4051...z" p-id="2331"></path>
</svg>
</div>
除了上面这个方式之外,还可以通过今天介绍的aria-label来创建可访问性名称和可访问性描述:
<div class="button" tabindex="0" role="button" aria-label="首页">
<svg t="1602911663963" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2330" width="200" height="200">
<path d="M982.028557 404.405174 573.3..." p-id="2331"></path>
</svg>
</div>
“VoiceOver”向用户呈现:
首页,按钮
当在<button>上或role="button"上使用aria-label时,属性的内容将覆盖按钮内的内容,可以作为可访问性名称。这也意味着,如果<button>或role="button"中有一个图标或甚至其他文本内容,该内容将不再作为按钮可访问性名称。也就是说,这个时候按钮的可访问性名称是由aria-label属性提供的。

另外,aria-label除了可以使用role="button"之外,还可以使用在<svg>元素上,并将<svg>上的aria-hidden(或设置为false),但我们确实希望通过focusabled="false"来防止它接到不需要的焦点。
<div class="button" tabindex="0" role="button">
<svg t="1602911663963" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2330" width="200" height="200" aria-label="首页" focusable="false">
<path d="M982.02...z" p-id="2331"></path>
</svg>
</div>

在Web中使用SVG构建图标时,我们可以在内敛的SVG中使用其<title>标签,用来描述SVG(有点类似于HTML中的<title>)标签。加上我们前面说过,如果我们在HTML文档中有文本内容,我们就不应该使用aria-label标签,更建议使用aria-labelledby。比如我们基于上面的示例来做一个简单的调整:
<div class="button" tabindex="0" role="button">
<svg t="1602911663963" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2330" width="200" height="200" focusable="false" aria-labelledby="title">
<title id="title">首页</title>
<path d="M982.028557 404.405174 5...388z" p-id="2331"></path>
</svg>
</div>
“VoiceOver”将向用户呈现:
首页,按钮
AOM中可以看得出,<svg>的<title>变成了按钮的可访问名称:

现来看一个复杂的一点的示例,比如一个卡片,我们希望在整个卡片得到焦点时,“VoiceOver”能很好的告诉用户关于卡片的信息,而且焦点也不会过于冗余。
<div class="card">
<div class="midai__object--card">
<img src="//gw.alicdn.com/bao/uploaded/i4/2580703436/TB2LI8xyOpnpuFjSZFkXXc4ZpXa_!!2580703436.jpg_360x10000Q50.jpg" alt="6条装,南韩珍珠吸水抹布">
</div>
<div class="media__body--card">
<h5 class="media__title">珍珠粒抹布吸水不掉毛加厚擦玻璃布擦家具桌子拖地板洗碗布清洁巾</h5>
<div class="media__des">
<span class="badge">金币抵3%</span>
</div>
<div class="media__price">
<span>¥</span>
<span>109</span>
<span>.00</span>
</div>
</div>
</div>
实测时,“VoiceOver”分别用多个焦点给用户呈现信息:
在div.card上并没有任何可访问性名称和可访问描述:

这样的结果也是能想到的,因为div.card并不是可聚集也不是可访问的元素。因此,希望让div变成可访问元素(或具有可访问性名称),那就需要显式给div.card添加role角色。因为没有card这样的色角,我尝试着在div.card上加上role="region":

同样没有任何可访问性名称和描述。接着我尝试着在div.card使用aria-labelledby,并且绑定h5的id:

有可访问性名称,但焦点还是冗余,为此将div.card的子元素显式加上aria-hidden="true",但这个时候整个卡片都没有任何焦点,甚至它的子元素以及后代子元素都没有焦点。为了让卡片有焦点,在div.card上显式设置tabindex的值为-1。

我把tabindex从0、-1和1都尝试了一遍,卡片还是没有焦点。为此,尝试着给div.card的role角色换成option。经过测试后,整个卡片只有一个焦点:

当卡片得到焦点时,“VoiceOver”会把h5的信息呈现给用户。换句话说,这个离我们期望的结果越来越近了。为了能把另外的信息也呈现给用户,我使用aria-describedby来绑定div.card的子元素的id,把期望呈现给用户的信息都关联起来:
<div class="card" role="option" tabindex="-1" aria-labelledby="a11y__card--title" aria-describedby="a11y__badege a11y__price">
<div class="midai__object--card" aria-hidden="true">
<img src="//gw.alicdn.com/bao/uploaded/i4/2580703436/TB2LI8xyOpnpuFjSZFkXXc4ZpXa_!!2580703436.jpg_360x10000Q50.jpg" alt="6条装,南韩珍珠吸水抹布">
</div>
<div class="media__body--card" aria-hidden="true">
<h5 class="media__title" id="a11y__card--title">珍珠粒抹布吸水不掉毛加厚擦玻璃布擦家具桌子拖地板洗碗布清洁巾</h5>
<div class="media__des">
<span class="badge" id="a11y__badege">金币抵3%</span>
</div>
<div class="media__price" id="a11y__price">
<span>¥</span>
<span>109</span>
<span>.00</span>
</div>
</div>
</div>
“VoiceOver”呈现给用户的信息如下:
珍珠粒抹布吸水不掉毛加厚擦玻璃布擦家具桌子拖地板洗碗布清洁巾,金币抵3% ¥109.00

对应的AOM截图如下:

更多的时候,点击整个卡片可以进入到对应的详情页,为此我们可以将role改成link,在“VoiceOver”中呈现给用户的时候,整个div.card也只有一个焦点,并且也不会有冗余的子焦点:
就上面视频演示的结果,已达到我们期望的效果了。不过,很多时候,在应用中除了单独的卡片应用场景时,还会有列表的场景。
“VoiceOver”中呈现给用户的效果如下:
另外,有些卡片还是带有<button>的,比如:

针对这样的场景如何来做呢?如果你感兴趣的话,可以尝试一下。
小结
在这篇文章中我们主要和大家一起探讨了如何使用aria-label、aria-labelledby和aria-describedby属性给用户,特别是使用屏幕阅读器的用户提供更好的体验。比如避免焦点过于冗余,而且还能把关键信息呈现给用户。通过文章中的示例我们可以发现,并不是所有元素都支持,而且不同的用户代理和ATs技术(比如屏幕阅读)对他们的解析都会有所不同。为此文章中的示例都是“VoiceOver”测试的结果,并不能代表最终的所有结果。如果你在这方面有经验,欢迎在下面的评论中与我们共享。