display:none、visibility:hidden 和 opacity:0 有什么区别?
display:none 让元素不参与布局、不渲染,也会从无障碍树中移除;visibility:hidden 不绘制元素但保留它占的位置,不能获得焦点,子元素设置 visibility:visible 还能显示;opacity:0 只是完全透明,位置、点击和焦点都还在。
简答
三种写法都能让元素看不见,区别在看不见以后还剩下什么。
display: none最彻底,元素不生成盒子,不占位置,后面的内容会顶上来,读屏器也读不到它。visibility: hidden只是不画出来,位置还空在那里,布局不变;它不能被 Tab 键聚焦,也会从无障碍树中移除,读屏器读不到它;比较特别的是,它的子元素如果写了visibility: visible,还能单独显示出来。opacity: 0只是变成完全透明,位置、点击、键盘焦点都还在,用户可能点到一个看不见的按钮。性能上,
display在显示和隐藏之间切换,会改变布局,要重新排版;visibility不改变布局,只需要重新绘制;opacity的变化如果元素在单独的合成层上,只需要重新合成,所以做淡入淡出动画用opacity。要注意,只想在视觉上隐藏、但仍要让读屏器读到的内容,比如图标按钮的文字说明,前两种都会让读屏器读不到,第三种又会留下能点到的透明区域。常用一组“视觉隐藏”样式,把元素缩成 1 像素并裁剪掉,让它看不见但留在无障碍树里。
实际对比一下
下面三个元素高度都是 20px,分别用三种方式隐藏:
<div style="display: none">A</div>
<div style="visibility: hidden">B <span style="visibility: visible">子元素</span></div>
<div style="opacity: 0" onclick="alert('被点到了')">C</div>
<button style="visibility: hidden">按钮 1</button>
<button style="opacity: 0">按钮 2</button>
在 Chrome 里测量:A 的高度是 0,B 和 C 都还是 20px,说明后两者仍然占着位置。B 里面的“子元素”能显示出来,因为 visibility 是继承属性,子元素可以覆盖它;display: none 和 opacity 都做不到,父元素隐藏了,子元素怎么设都看不见。
C 透明了,用脚本点击它照样触发 onclick,真实用户点到那块区域也一样。按钮 1 调用 focus() 后没有获得焦点,按钮 2 却获得了,用户按 Tab 键会停在一个看不见的按钮上,这是无障碍问题。
只让读屏器能读到
.visually-hidden {
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}
元素被缩成 1 像素并裁掉,屏幕上看不见,但它仍然在无障碍树里。比如一个只有放大镜图标的搜索按钮,里面放一个带这个类名的“搜索”文字,读屏器就能读出按钮的用途。这段样式来自 The A11Y Project。MDN 在 display 和 visibility 页面也提醒:display: none 和 visibility: hidden 都会把元素及其后代从无障碍树中移除,想隐藏又想让辅助技术读到,应该用这类组合样式。要注意,按钮、链接这类能获得焦点的元素如果用了这个类,聚焦时要让它重新显示出来,否则键盘用户会停在一个看不见的地方。
常见追问
hidden 属性和 display: none 一样吗?
效果基本相同。MDN 的说法是浏览器可以用
display: none来实现hidden状态,主流浏览器的默认样式表正是这样做的。因为它只是一条默认样式,如果你给元素写了display: flex之类的样式,就会覆盖掉它,hidden就失效了,可以在全局样式里加[hidden] { display: none !important; }。
为什么 display: none 不能做过渡动画?
display是离散属性,只能在几个值之间跳变,没有中间状态,以前过渡对它不起作用。现在可以配合transition-behavior: allow-discrete和@starting-style实现,见《@starting-style 和离散属性过渡解决了什么问题?》。
opacity 小于 1 为什么会影响 z-index?
opacity小于 1 时元素会创建新的层叠上下文,它和子元素作为一个整体参与层叠,子元素的z-index再大也跳不出去,见《为什么 z-index 设置很大,元素还是被遮挡?》。