聊聊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”测试的结果,并不能代表最终的所有结果。如果你在这方面有经验,欢迎在下面的评论中与我们共享。