对于依赖屏幕阅读器操作网页的用户来说,按钮的可访问名称(accessible name)是他们理解交互元素的第一道信息。NVDA 作为 Windows 平台上使用最广泛的免费屏幕阅读器,对 aria-label 的支持并不是简单的“有就朗读”,而是遵循一套复杂的命名优先级计算规则。很多开发者遇到过这样的情况:按钮上明明写好了 aria-label,但 NVDA 在浏览模式下偏偏读出的是按钮内部的全角空格或默认文本,甚至在表单模式下完全不发声。这种不一致的行为让不少人误以为 NVDA 对 aria-label 支持有 bug,实际情况却要复杂得多。

理解 NVDA 的命名优先级计算机制
要弄清楚 NVDA 为什么不读 aria-label,第一步得理解可访问名称是怎么算出来的。浏览器(比如 Chrome 的 Accessibility Tree 或 Firefox 的 Accessible Name and Description Computation)会把属性排序,然后按优先级从高到低选用第一个非空值。这个优先级大致是:aria-labelledby 指定的元素文本优先于 aria-label,aria-label 优先于内部文本内容(也就是按钮被包裹的文字),内部文本又优先于 title 属性。
这个规则初看很简单,但坑在于“非空值”这个概念。如果 aria-labelledby 引用了一个隐藏或者在 DOM 中不存在的元素,NVDA 的处理方式在不同浏览器内核下并不完全一致。有些情况下,浏览器会认为这个引用是“无效的”,从而回退到下一级——也就是 aria-label;但另一些情况下,可能因为被引用元素虽然隐藏但依然存在一个空文本节点,导致整个名称计算意外截断,让 NVDA 得到空名称,最后读取内部文本。测试这类场景时,不要用单一的 Chrome + NVDA 组合下结论,还要在 Firefox 下对比验证。
另外一个容易被忽略的细节是:aria-label 属性值是区分不了中文逗号和英文逗号、全角空格和半角空格的。如果属性写的是 aria-label=" "(纯空格),不少浏览器会视为空值,NVDA 就会放弃这个名称源。开发时建议用实际中文文本,不要用空字符串制造“清理按钮”的效果。再看下面这段代码:
<button aria-label="关闭对话框">×</button>
在这个示例中,按钮内部只有一个乘号字符作为视觉图标。正常情况下 NVDA 会朗读“关闭对话框,按钮”。但如果你在同一个页面上又对同一个按钮设置了 aria-describedby 指向一个空的隐藏元素,许多版本的 Firefox 会把描述信息算法和名称算法交错,导致 NVDA 读出“× 按钮”而非预期名称。写代码时尽量保持名称属性和描述属性的目标元素都明确存在、内容有效,能显著减少这种意外回退的发生。
浏览模式与聚焦模式的差异解析
NVDA 的浏览模式和聚焦模式是它区别于其他屏幕阅读器最典型的交互方式。在浏览模式下,用户用上下方向键移动虚拟光标,此时 NVDA 是从浏览缓冲区读取整段渲染后的文本;在聚焦模式下,NVDA 把按键事件直接透传给页面,方便用户操作输入框或使用箭头键选择选项。对按钮而言,Tab 键聚焦时 NVDA 大概率会读出可访问名称,但如果是通过虚拟光标扫到这一段内容,NVDA 未必会单独解析按钮的 aria-label,而可能直接朗读按钮内部的可见文本。
如果你测试时按 Tab 键让按钮真正获得焦点,NVDA 读的是 aria-label,说明属性本身没问题。可当用户用浏览模式的上下键扫读页面时,NVDA 会“合并”一些元素——例如把按钮和邻近的文本段当成一行,这时它优先拼出视觉上可见的文字。这是 NVDA 的设计取舍,它在扫读模式下更看重屏幕上的可见信息是否匹配,而不是每个控件的 API 名称。因此,要判断 aria-label 是否生效,必须区分“焦点到达时”和“虚拟光标经过时”两种状态。
在测试按钮时,建议先用 Tab 键进入页面,然后按 Insert + Tab 读出当前聚焦对象。如果读的是自定义名称,就说明 NVDA 已经正确识别。这时再按小键盘 0 或 Numpad 0 锁定焦点跟随,确保 NVDA 不出现“焦点在按钮上但虚拟光标停在别的地方”的错位情况。下面这组代码可以辅助你判断按钮焦点行为:
<button aria-label="发布评论" id="publishBtn"> 发表 </button>
在 Chrome 中,用户按 Tab 聚焦到这个按钮后,NVDA 通常朗读“发布评论,按钮”。这证明它的名称计算已经命中 aria-label。但假如你使用了 role="button" 的 div 元素,情况就大不一样。div 本身没有原生按钮的键盘交互语义,NVDA 在浏览模式下可能直接跳过这个元素,除非你给它加上 tabindex="0" 并按 Enter 或空格键触发点击。仅依赖 aria-label + role 的 div 可访问性风险较高,能使用原生 button 就不要用 div 替代。
aria-hidden 与兄弟节点的干扰因素
有些按钮看似设置了正确的 aria-label,但 NVDA 就是读不出来,问题出在按钮的子节点或兄弟节点上。举个例子,如果按钮内部有一个 <span aria-hidden="true">✓</span>,这个 span 的图标文本不会影响名称计算,这是符合预期的;但假如你在外层容器上设置了 aria-hidden="true",却忘了把这个按钮包含在内,又或者反过来——把按钮本身设成 aria-hidden="true",那么 NVDA 连这个按钮的存在都不会知道,还谈何标签朗读。
另一个常见干扰来自包裹文本节点的父子关系。NVDA 在计算可访问名称时会遍历该元素内部的所有后代节点,把它们的文本合并成一个字符串。但因为 aria-label 的优先级高于内部文本,一般情况下内部文本再长也不会覆盖 aria-label。不过,有一种例外是:当按钮处于一个 label 标签的包裹之下,并且 label 的 for 属性又指向按钮 id 时,标签内的可见文字反而可能成为名称来源,与 aria-label 产生竞争。无谓的 label 包裹按钮是很多无障碍审查报告里出现频率最高的问题之一。
如果你在处理弹窗的关闭按钮,标准写法往往是这样:
<div role="dialog" aria-labelledby="dialogTitle">
<h2 id="dialogTitle">设置</h2>
<button
type="button"
aria-label="关闭"
class="close"
>
<span aria-hidden="true">×</span>
</button>
</div>
这里即便 span 里是乘号实体,因为 span 属性是 aria-hidden="true",它的文本不会参与按钮名称拼接,所以 NVDA 最终读到“关闭,按钮”。这个做法是目前主流框架里最稳妥的关闭按钮写法。同理,如果按钮内部的视觉文本是“×”,并且你不想让 NVDA 读那个叉号,就不要过度依赖 CSS 的 font-size: 0 来隐藏文本,因为剪贴板复制和 NVDA 的某些扫读模式仍然会拿到底层文本“×”,与 aria-label 产生不一致的朗读效果。
动态更新内容时的 NVDA 朗读时机
动态单页应用最常见的无障碍缺陷之一,就是按钮的 label 在加载完成后又被 JavaScript 修改了,但 NVDA 没有收到对应的可访问名称变更事件。例如一个“下载报告”按钮,点击后想要变成“正在下载,请稍候”,开发者直接在 JS 里设置 btn.setAttribute('aria-label', '正在下载')。这个操作本身会触发 accessibility 的 mutation 事件,可 NVDA 并不会像读一条 live region 那样主动播报新的文本变化。它的表现是:当用户下次聚焦到这个按钮时,读出的已经是新名称;但如果焦点一直在按钮上,它未必会即时重读。
要解决“名称变化但 NVDA 没有及时反馈”的问题,你应该在变化的同时把焦点移走或触发一个播报提示。最常见的模式是点击后让按钮设置 disabled,同时把焦点移到一个非交互状态提示元素上,配合 aria-live="polite" 播报状态。NVDA 对 aria-live 的支持是成熟的,polite 适合非打断性提示,assertive 适合必须立刻告诉用户的情况。下面演示一个典型的“点击后更新名称且支持 NVDA 朗读状态”的完整示例:
<button id="downloadBtn" aria-label="下载报告">
下载报告
</button>
<div id="downloadStatus" aria-live="polite"></div>
<script>
const btn = document.getElementById('downloadBtn');
const status = document.getElementById('downloadStatus');
btn.addEventListener('click', () => {
btn.setAttribute('aria-label', '正在下载');
btn.disabled = true;
status.textContent = '正在下载,请稍候';
});
</script>
这段代码中,用户触发点击后 NVDA 会通过 live region 播报状态文本,按钮名称的变更也会被记录,下次进入按钮时读取的是“正在下载,按钮,不可用”。用这种方法,既避免了焦点被强制移动造成的流程打断,又保持了可访问名称的同步更新。注意 disabled 属性会让按钮从 Tab 序列中移除,所以下载完成后要恢复按钮可聚焦状态,并把焦点主动移动到该按钮上,保证键盘用户的操作路径完整。
可访问名称的替代写法与国际化考虑
aria-label 并不是按钮命名的唯一方案,也未必是最便于维护的方案。如果你的按钮内部有可见文本,并且这个文本已经能清楚地表达动作,那么根本不需要加 aria-label。给“确认”按钮硬加一个 aria-label="确定按钮" 反而会引起 NVDA 朗读内容的冗杂。更值得推荐的做法是用 aria-labelledby 指向一个不可见、但对屏幕阅读器来说依然可读的标题元素,这样既满足视觉上的简洁要求,也能让名称与文档结构产生关联。
在国际化项目里,把“取消”“关闭”这类词汇直接写死在 aria-label 中会带来翻译问题。较好的策略是使用数据属性或前端框架的国际化函数绑定生成属性值。比如在 React 中可以这样处理:
const closeLabel = intl.formatMessage({ id: 'button.close' });
return (
<button type="button" aria-label={closeLabel}>
<span aria-hidden="true">×</span>
</button>
);
这里 aria-label 来自国际化资源文件,而不是硬编码的中文或英文。这样一来,NVDA 用户在切换 NVDA 的语言界面时,也能与页面返回的标签文本保持一致。相比依赖内部不可见文本的覆盖技巧,这个方案把可访问名称的生成和维护集中到一处,代码审计时一目了然。同时要注意在无障碍树中,NVDA 对于 aria-label 里的换行符和多余空格的处理比较宽松,但个别版本遇到制表符会出现停顿甚至截断,所以建议把多语言文本统一压缩成单行字符串,移除前导空格。
最后提一个容易踩坑的细节:当按钮同时拥有 title 属性和 aria-label 时,NVDA 绝大多数情况下会优先使用 aria-label,但把鼠标悬停在按钮上时,浏览器会显示 title 的工具提示,而屏幕阅读器用户听的是 aria-label,这就可能产生视觉信息与听觉信息的错位。最好的做法是保持 title 为空,或者让 title 和 aria-label 的文字保持一致。如果 title 的出现只是为了补充说明按钮行为,建议使用 aria-describedby 指向一个描述文本,这样 NVDA 会先读出名称,再以“描述”的形式补充额外说明,用户听到的信息更结构化。注意描述信息在 NVDA 中通常与名称分开朗读,并且可以通过设置关闭描述播报,因此不要把关键操作提示放在描述里。
要验证最终的实现是否可靠,你可以按 NVDA + 方向键的浏览模式逐句听页面,也可以直接打开 Chrome 的 Accessibility 面板查看当前按钮的 computed name 是否为 aria-label 的值。在 Firefox 的“无障碍”检查器里同样可以检视按钮的名称计算来源。如果浏览器开发者工具显示的名称正确,但 NVDA 依然不读,就应该检查一下 NVDA 是否处于浏览模式、焦点是否真正在按钮上,以及是否有其他浏览器扩展抢占了 Ctrl 或 Insert 快捷键。排除这些外部因素后,再看代码里是否存在重复的 aria-label 覆盖或 ARIA 角色的错误使用。
NVDAaria-label屏幕阅读器修改时间:2026-08-20 03:01:35