display: none 和 visibility: hidden 会使屏幕阅读器完全忽略元素,因其分别移出渲染树或被标记为不可访问;真正可访问的 visually-hidden 需保留 DOM 且对读屏器可见,推荐使用绝对定位+1px尺寸+clip/clip-path 等组合,并注意父容器 overflow、scoped 样式不解决可访问性、dialog 内隐藏文本需语义合理嵌套。

为什么 display: none 或 visibility: hidden 会让屏幕阅读器“失明”
这两个 CSS 声明会直接切断元素与辅助技术的连接。display: none 把元素彻底移出渲染树,visibility: hidden 虽保留布局占位,但浏览器明确将其标记为“不可访问”。结果是:视觉上隐藏了,读屏器也完全跳过——这不是“隐藏”,是“删除”。
常见翻车场景:
- 用
display: none隐藏表单错误提示,键盘用户 tab 到输入框后听不到任何反馈 - 给图标按钮加
visibility: hidden再塞文字,结果按钮只有图形、无名称,aria-label又没写,NVDA 直接报“未命名按钮”
真正可用的 visually-hidden CSS 怎么写
W3C 推荐的最小可行组合必须同时满足两个条件:保留在 DOM 中 + 对读屏器可见。最稳妥写法是:
visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
注意几个硬性约束:
立即学习“前端免费学习笔记(深入)”;
-
width和height不能为0,否则部分旧版读屏器(如 JAWS 2018)会忽略该元素 -
clip的旧写法rect(0, 0, 0, 0)兼容性好,但现代项目建议用clip-path: inset(50%)并加@supports回退 - 绝对定位本身没问题,但如果父容器设了
overflow: hidden,隐藏内容会被裁掉——得检查层级结构
scoped 样式或 CSS Modules 不能替代可访问性隐藏
组件化工具(如 Vue 的 <style scoped> 或 React 的 CSS Modules)解决的是类名冲突,不是可访问性问题。它们生成的样式依然可能包含 display: none 或 visibility: hidden,照样让屏幕阅读器失效。
典型误用:
- 在
Button.module.css里写.sr-only { display: none; },再在 JSX 里className={styles.sr-only}—— 效果等同于裸写display: none - 用 scoped 样式给弹窗遮罩层加
visibility: hidden,结果键盘焦点锁不住,Esc 键失效 - 以为 “用了 CSS Modules 就安全了”,没重审所有带
hidden行为的样式声明
dialog 元素里藏文本要特别当心
<dialog> 是原生模态容器,但它对内部可访问性更敏感。如果在 <dialog> 里用视觉隐藏文本补充说明,必须确保:
- 隐藏文本是
<dialog>的直接子元素或语义合理嵌套(比如放在<header>里),不能被overflow: hidden的 wrapper 截断 - 不要给
<dialog>自己加aria-hidden="true"—— 这会把整个弹窗对读屏器屏蔽,包括所有子内容 - 若用
aria-label替代隐藏文本,别同时放两者,否则读屏器重复播报(aria-label会覆盖全部子节点语义)
最难的不是写出那几行 CSS,而是每次加隐藏文本前,都得确认:这个信息是否在键盘焦点流、触控操作流、读屏播报流里都一致可达。漏掉任意一条路径,就等于把缺陷换了个方式暴露出来。



















