页面内badge可访问性核心是aria-live与aria-label协同:aria-label注入语义文本并动态更新,aria-live="polite"配合aria-atomic="true"确保屏幕阅读器完整播报变更内容。

aria-live + aria-label 是页面内 Badge 的可访问性核心
网页里显示“未读 3 条”的角标(比如购物车右上角红点+数字),屏幕阅读器默认是读不到的——它只读 DOM 文本,不读 CSS 伪元素或背景图。所以不能只靠 span 加样式,必须让语义和状态同步更新。
常见错误现象:用户听到“购物车按钮”,但没听到“3 条未读消息”;或者数字变了,屏幕阅读器完全静音。
- 必须用
aria-label把数字注入宿主元素(如按钮),例如:<button aria-label="购物车,3 条未读消息">?</button> - 数字变更时,要同步更新
aria-label值,不能只改span内容 - 如果数字为 0 或空,
aria-label应退化为无数字版本(如“购物车”),避免读出“0 条未读消息”这种冗余信息 - 若角标文本独立于按钮(比如用
span浮在右上角),需用aria-describedby关联,并确保该span有role="status"或包裹在aria-live="polite"区域中
为什么不能只靠 CSS 角标 + title 属性
title 属性在多数屏幕阅读器中不朗读(尤其 ChromeVox、NVDA 默认关闭),且 iOS Safari 和 Android WebView 完全忽略它;CSS 伪元素(::after)生成的内容对辅助技术不可见,等于“视觉存在,语义消失”。
更隐蔽的问题是:当角标仅通过 display: none 或 visibility: hidden 隐藏时,屏幕阅读器仍可能将其纳入朗读流,造成焦点混乱或重复播报。
- 数字为 0 时,应彻底从 DOM 中移除角标
span元素,而不是设display: none - 若必须保留占位,用
aria-hidden="true"+class="sr-only"组合,确保视觉隐藏且语义剔除 - 避免在
title里塞数字,它无法响应动态更新,也不支持语音节奏控制
动态更新时容易漏掉的焦点与原子性控制
单纯改 aria-label 不够——若用户正聚焦在购物车按钮上,数字突变后,屏幕阅读器未必主动播报,尤其在 NVDA 的“焦点模式”下。
这时需要 aria-live 区域兜底,但要注意初始化时机和原子性设置:
-
aria-live容器必须在页面加载时就存在于 DOM 中(不能 JS 动态创建),否则 NVDA/JAWS 会忽略 - 推荐将
aria-live="polite"区域放在开头,用aria-atomic="true"确保整段文本被完整朗读(避免只读“3”而漏掉上下文) - 触发播报不是靠改属性,而是向该区域
textContent插入新字符串,例如:liveRegion.textContent = '购物车更新:3 条未读消息' - 插入后立即清空内容(或加空格),否则下次更新可能因内容未变而不触发朗读
桌面 PWA 角标和 Notification API 不解决页面内可访问性
有人试图用 Notification API 或 navigator.setAppBadge() 替代页面内角标,这是混淆层级:前者是系统级弹窗,后者是桌面图标 badge,两者都绕不开当前页面的可访问性缺口。
用户在页面内操作时,根本看不到系统通知;而桌面角标在移动端无效、在 Safari 完全不支持,更无法被屏幕阅读器感知。
- 页面内角标(DOM 元素 + ARIA)是唯一能被键盘用户、屏幕阅读器用户实时感知的途径
-
Notification API只能作为补充提醒(需用户授权、HTTPS 环境),不能替代页面内语义表达 - 不要把
aria-live区域写在 React/Vue 组件内部——它必须是全局稳定节点,否则组件卸载重挂会导致读屏中断
最易被忽略的一点:角标数字变更时,不仅要更新文本和 ARIA,还要检查父容器是否设置了 overflow: hidden(比如圆形头像),这会让角标被裁切——视觉失效的同时,用户可能误判“没新消息”,而屏幕阅读器又没播报,形成双重盲区。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











