你有没有遇到过这种糟心事?辛辛苦苦写了个组件,键盘操作顺滑,ARIA属性加得满满当当,自动化测试工具一片绿,结果真有人用屏幕阅读器访问时,愣是不知道怎么触发它。前段时间我就干了这么件蠢事,一个看起来再正常不过的按钮,居然把屏幕阅读器用户给整不会了。
起初为了给按钮整点不一样的样式,让它看起来像个链接,又懒得去改那套已经写好的样式类,就顺手给这个按钮加了个 role="link"。想着反正都是点一下,加个角色属性又不影响功能,没准儿还能让语义更清晰点。结果现实啪啪打脸,当自己戴上耳机,关掉显示器,纯靠键盘和NVDA操作时,才发现这个按钮的行为完全不在预期内。按空格键没反应,屏幕阅读器播报的角色也含含糊糊,既不像按钮也不像链接,整个一四不像。
这回踩坑让我彻底明白一件事:语义HTML自带的无障碍能力,比我们想象的要强大得多,而ARIA这东西,用好了是助攻,用不好就是添乱。很多新手(包括当时的我)容易把ARIA当成万能补丁,哪里不对劲就往上贴,结果把好好的原生语义给覆盖了,让浏览器和辅助技术都陷入迷茫。
语义HTML的真实力
先别急着加属性,看看原生<button>到底给了多少好东西。就下面这一行代码:
<button>保存修改</button>别小看这行代码,浏览器和辅助技术联手给打包送了一套完整交互:
- 键盘操作自动搞定:
Tab键聚焦,Enter键和空格键都能触发点击 - 焦点行为正确:开发者不用写一行JavaScript,聚焦顺序和样式完全符合预期
- 角色含义明确:屏幕阅读器直接告诉你这是个按钮
- 播报一致性高:不同屏幕阅读器对按钮的解读方式基本一致
在加任何ARIA之前,先用原生标签把这个基础体验跑通。这时候啥额外代码都没写,无障碍体验就已经及格了。
别急着加属性
这个阶段最重要的就是管住手,先用原生标签把交互搭起来。像上面那个保存按钮,加个类名用来写样式就完事儿了:
<button class="cta">保存修改</button>这时候打开浏览器的无障碍面板,能看到元素角色是button,可点击状态、焦点状态都一目了然。千万别在这个阶段就开始琢磨“哎呀要不要加个role”这种问题,先把原生行为观察透了再说。
测试只需要三步:
- 纯键盘操作:按
Tab键能不能聚焦到?按Enter和空格能不能触发? - 屏幕阅读器听听:它怎么播报这个控件?
- 看焦点顺序:按
Tab键在页面里移动,顺序合不合理?
把这些摸清楚了,就得到了一个行为基准。万一后面加东西加出问题,能立刻定位到是后面加的代码搞的鬼。
好心办了坏事
问题恰恰出在想着“优化”一下的时候。当时设计稿要求某个按钮样式要像链接,又不想动已有的样式类,就想着用role属性来暗示一下角色:
<button class="cta" role="link">保存修改</button>这么一改,表面上看啥问题没有。自动化工具还是静悄悄,样式也没崩。但真实用起来就露馅了:
- 空格键失灵:原生按钮的空格激活功能被覆盖了
- 角色播报混乱:屏幕阅读器先读按钮,又读链接,用户根本不知道这是个啥
- 交互预期错位:用户以为是个链接,结果跳转逻辑没按链接走
问题出在哪儿?ARIA改的是语义,不是行为。给按钮加上role="link",只是在告诉屏幕阅读器“这东西是个链接”,但浏览器不会因此自动给这个元素加上链接的键盘行为。结果就尴尬了:屏幕阅读器说这是个链接,但按链接的操作方式(比如回车)可能不灵,按按钮的操作方式(空格)也被覆盖了。
<!-- 下面这行代码看着没毛病,其实埋了坑 -->
<button role="link">点我</button>
<!-- 空格键:我该不该工作? -->
<!-- 屏幕阅读器:这到底是个啥? -->回归语义之路
解决方式简单到让人想撞墙:删掉那个画蛇添足的role属性。如果样式需求确实要区分,就用更纯粹的样式类来处理:
<button class="cta cta-alt">保存修改</button>这样改完之后,所有问题都消失了。空格键恢复正常,屏幕阅读器播报清晰,用户反馈也确认问题解决。整个修复过程就是做减法,把不该加的ARIA删掉,让浏览器做它擅长的事。
给样式类命名的时候也得注意,别用button-link这种混淆语义的名字,直接用cta-alt、variant-secondary这类纯粹描述视觉的命名就行。把样式和语义彻底解耦。
ARIA的正确打开方式
这次经历后,给自己定了个铁律:先用原生HTML表达意图,测试没问题了再考虑ARIA,而且ARIA只能用来补充状态,不能用来覆盖角色。
举个例子,一个折叠面板需要告诉屏幕阅读器当前是展开还是收起状态。这时候用原生<button>加上aria-expanded属性就非常合适:
<button id="toggle" aria-expanded="false">
查看详情
</button>
<div id="panel" hidden>
这里是折叠面板里的详细内容
</div>配合一点JavaScript来切换状态:
const button = document.getElementById('toggle');
const panel = document.getElementById('panel');
button.addEventListener('click', () => {
const expanded = button.getAttribute('aria-expanded') === 'true';
button.setAttribute('aria-expanded', !expanded);
panel.hidden = expanded;
});这个例子中,按钮还是那个按钮,角色没变,但通过ARIA补充了额外的状态信息。这才是ARIA该待的位置:补充原生元素表达不出来的动态信息。
| ARIA使用场景 | 具体方式 | 典型例子 |
|---|---|---|
| 补充状态 | aria-expanded | 折叠面板开关 |
| 动态更新 | aria-live | 表单提交反馈 |
| 自定义控件 | role组合 | 自定义滑块 |
| 关系表达 | aria-describedby | 表单错误提示 |
核心原则就一条:如果ARIA在做“重活”,那大概率是HTML选错了。让语义HTML打好地基,ARIA只负责地基上那些原生表达不了的东西。
