译在网页中隐藏元素
Category(分类): HTML Status: 未知
本文基于 Ahmad Shadeed 的《Hiding On The Web》翻译整理,并对其中的部分结论进行了勘误与补充。
在平时的开发工作中,经常会有隐藏元素的需求。比如:一个按钮需要在桌面端隐藏、在手机端显示;一个导航栏需要在手机端隐藏、在桌面端显示。
“隐藏”并不像字面上那么简单,它至少包含三层不同的含义:
- 元素完全从文档流中移除,既不可见,也无法交互;
- 元素只是视觉上隐藏,但使用屏幕阅读器(screen reader)等辅助技术(assistive technology,简称 AT)仍然能访问;
- 元素视觉上可见,但对屏幕阅读器隐藏,即屏幕阅读器无法访问。
选择哪种方式,取决于你是想“视觉隐藏”“交互隐藏”还是“无障碍隐藏”。本文会逐一介绍用 HTML / CSS 隐藏元素的各种方法,并说明它们对可访问性、动画和资源加载的影响。
HTML5 hidden 属性
hidden 是一个布尔属性。浏览器为它内置了默认的用户代理样式,效果等价于 display: none,因此被它修饰的元素既不渲染,也不占据布局空间,同时会从可访问性树中移除。
<img src="cover.jpg" hidden alt="文章封面">
hidden 的一个好处是:即使 CSS 因某种原因没有加载成功,元素也会被隐藏,因为它的隐藏效果来自浏览器内置样式,而不依赖外部 CSS。
来看一个例子:页面上有一段包含标题、图片和描述的内容,我们希望图片只在视口宽度大于 400px 时才显示。这里先给 <img> 加上 hidden 属性作为默认隐藏状态:

然后用一段 CSS,让带 hidden 属性的图片在大视口下重新显示:
img[hidden] {
display: none;
}
@media (min-width: 400px) {
img[hidden] {
display: block;
}
}
下面是在大视口(大于 400px)下的渲染效果:

注意:由于
hidden等价于display: none,它无法用于过渡动画(display不是可动画属性)。如果你需要隐藏/显示的动画效果,应该使用下面会讲到的opacity、visibility或transform。
hidden 属性对可访问性的影响
hidden 会将元素完全从页面隐藏,屏幕阅读器无法访问。如果只是为了视觉表现,应避免使用它。
CSS display 属性
每个元素都有一个默认的 display 值,可能是 inline、block、inline-block、table 等。把 display 设为 none 就能隐藏元素,并且该元素的所有后代都会随之一并隐藏。
把上面的例子换成 display: none:
img {
display: none;
}
@media (min-width: 400px) {
img {
display: block;
}
}
display: none 会把元素从文档流中完全移除,就好像它根本不存在一样——原来占据的空间也随之消失,周围的元素会重新排列。可以用一摞书来形象地说明:

把蓝色的那本书用 display: none 隐藏,它就从这摞书里完全消失了,原来占据的位置被后面的书填补。HTML 与此类似,说明文档流被改变了。下面的动画展示了蓝皮书被移除时的状况:

display: none 的图片还会加载吗?
这取决于浏览器实现和图片的 loading 属性,不能一概而论:
- 默认
loading="eager"的图片,部分浏览器在某些情况下仍可能发起请求; - 带有
loading="lazy"的图片,当其本身或某个祖先为display: none时,现代 Chrome / Firefox / Safari 通常不会发起请求,等到它真正可能显示时再考虑加载; display: none本质上是渲染/布局属性,不是可靠的“控制资源是否加载”的手段。
因此,==如果想可靠地避免图片加载,应该==:
- 使用原生
loading="lazy"; - 或者在真正需要时再通过 JavaScript 设置
src(配合data-src); - 不要依赖
display: none来“阻止” eager 图片的下载。
原译文断言“
display: none的图片一定会加载”,这个结论在当今主流浏览器中并不总是成立,使用时请以实际测试为准。
<style> 元素
有些元素默认就是 display: none 的,例如 <style>。如果把它设为 display: block,原本不可见的样式块就能显示在页面上:
<body>
<style>
.title { color: #000; }
</style>
</body>
style {
display: block;
}
如果再给 <style> 加上 contenteditable 属性,让样式块变得可编辑,效果会更有趣:

Demo(译注:在 Firefox 中能看到上面的效果,而在 Chrome 里是看不到的)
display: none 对可访问性的影响
与 hidden 属性一样,display: none 元素从页面中完全隐藏,屏幕阅读器也无法访问。
opacity
给元素设置 opacity: 0 后,元素及其后代都会在视觉上隐藏(这不是因为继承,而是 opacity 会整体作用于元素及其内容)。但这只是视觉上的隐藏:元素仍然占据布局空间,仍然可以接收鼠标事件,也仍然可以被键盘聚焦、被屏幕阅读器访问。
另外需要注意:opacity 小于 1 的元素会创建一个层叠上下文。

上图里,蓝皮书只是在视觉上隐藏了,它占据的空间仍然保留,这摞书的顺序没有发生变化,与 display: none 形成对比。
img {
opacity: 0;
}
把开头的例子用 opacity: 0 改写后,图片在视觉上消失了,但它占据的空间依然存在:

Dusan Milovanovic 指出,可以用 pointer-events: none 来禁用 opacity: 0 隐藏元素上的鼠标事件。这很重要——如果一个元素看不见,却仍然能响应点击、悬停或文本选择,肯定是很奇怪的:
img {
opacity: 0;
pointer-events: none;
}
opacity: 0 对可访问性的影响
使用 opacity: 0 隐藏的元素依然能被屏幕阅读器访问,也能被键盘聚焦。如果不想让它被交互,需要额外配合 visibility: hidden、inert 或 pointer-events: none 等。
visibility
visibility: hidden 与 opacity: 0 类似,元素会在视觉上隐藏,但不影响文档流,仍然占据布局空间。

蓝皮书从可视流中隐藏了,但没有影响这摞书的顺序。
==visibility 有一个特殊的行为:如果父元素设置了 visibility: hidden,那么它及其后代默认都不可见;但如果某个子元素显式设置 visibility: visible,那么这个子元素会重新变为可见==。

<article>
<h1>Spring is on the way</h1>
<img src="landscape.jpg" alt="">
<p><!-- 描述文字 --></p>
</article>
article {
visibility: hidden;
}
img {
visibility: visible;
}

上例中,<article> 使用了 visibility: hidden,而子元素 <img> 设为 visibility: visible,结果图片依然显示。也就是说,子元素可以重写父元素的 visibility。
此外,visibility 是可动画属性,可以配合 transition 实现显示/隐藏的过渡。这也是为什么后面“菜单动画”的推荐写法会选择它。
visibility: hidden 对可访问性的影响
visibility: hidden 的元素会从可访问性树中移除,屏幕阅读器一般无法访问到它,并且它通常无法被键盘聚焦(这点和 opacity: 0 不同)。
注意:如果隐藏元素的某个后代被显式设为
visibility: visible,该后代通常会重新进入可访问性树。不过各浏览器/辅助技术对此的实现并不完全一致,不建议依赖“父级隐藏 + 子级可见”这种模式来做无障碍处理。如果某块内容需要被屏幕阅读器读到,最好把它从隐藏的祖先中独立出来。
把元素移出视口(position)
另一种隐藏思路是:不改变元素的可见性,而是通过定位把它移到屏幕之外,或者把它的尺寸压缩为 0。一个典型场景是网页中的跳过导航(skip navigation)链接:

为了让链接默认不可见,可以把它绝对定位到视口之外:
.skip-link {
position: absolute;
top: -100%;
}
这里的 top: -100% 是相对于最近的定位祖先(包含块)的高度计算的,不一定等于视口高度——只有当包含块是初始包含块(近似视口)或其高度恰好等于视口高度时,才相当于“上移一个视口高度”。当链接被键盘聚焦时,再把它移回可见位置:
.skip-link:focus {
top: 0;
}
position: absolute | fixed 对可访问性的影响
移出视口的元素仍然可以被屏幕阅读器读到、被键盘聚焦,它只是视觉上不在屏幕内而已。
clip-path
==clip-path 用来创建一个裁剪区域,只有落在区域内的内容才可见,其余部分被隐藏。==

借助 clippy 工具可以直观地理解。下面的写法把多边形所有顶点都设在 (0, 0),裁剪区域退化为 0,图片也就完全不可见了:
img {
clip-path: polygon(0 0, 0 0, 0 0, 0 0);
}

也可以用一个半径为 0 的圆来代替多边形:
img {
clip-path: circle(0 at 50% 50%);
}

clip-path 实现的隐藏只是视觉上的,屏幕阅读器仍然可以访问,键盘也能聚焦。
操作 color 和 font-size
下面这两种手段不如前面的方法通用,但在特定场景下比较有用。
color: transparent
把文本颜色设为透明,只是视觉上隐藏了文字,适合用于“仅图标按钮”的文本部分。
font-size: 0
把字号设为 0,同样是视觉上的隐藏。
来看一个包含如下结构的按钮:
<button>
<svg width="24" height="24" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<!-- 路径数据 -->
</svg>
<span>Like</span>
</button>
目标是用一种可被访问的方式隐藏文本(只保留图标)。为此可以使用下面的 CSS:
button span {
color: transparent;
font-size: 0;
}
文本就这样被视觉隐藏了,但仍可被屏幕阅读器读出。

aria-hidden
给元素添加 aria-hidden="true" 后,它会从可访问性树中移除,用于提升屏幕阅读器用户的体验。需要注意的是,元素在视觉上依然可见。
<button>
Menu
<svg aria-hidden="true"><!-- 图标 --></svg>
</button>
这里是一个带文字和图标的菜单按钮。为了让 SVG 图标对屏幕阅读器隐藏,给它加上了 aria-hidden。
根据 MDN 文档,aria-hidden 的典型使用场景包括:
- 隐藏装饰性内容,如图标、分隔符、纯装饰图片;
- 隐藏重复的文字;
- 隐藏屏幕外的或折叠起来的内容。
⚠️ 关键警告:不要把 aria-hidden 用在可聚焦元素上
aria-hidden 只是把元素从可访问性树中移除,并不会阻止键盘聚焦。如果把 aria-hidden="true" 用在一个可交互、可聚焦的元素(或包含可聚焦后代的祖先)上,键盘用户依然能 Tab 到它,而屏幕阅读器却无法识别它,这会造成严重的可访问性问题。
<!-- ❌ 错误:键盘能聚焦,但屏幕阅读器读不到 -->
<button aria-hidden="true">删除</button>
<!-- ❌ 同样错误:后代链接仍然可聚焦 -->
<div aria-hidden="true">
<a href="/settings">设置</a>
</div>
正确做法:
- 装饰性、非交互内容 → 使用
aria-hidden="true"; - 当前不可用的内容或控件 → 同时从视觉和交互中移除,优先使用
hidden、display: none或visibility: hidden; - 整块暂时不可用的区域(如抽屉式导航、模态框背后的内容)→ 使用
inert属性,统一移除其后代的焦点与交互能力。
动画和交互
速查表
在进入示例之前,先回顾一下前面提到的各种属性。我从 CSS-Tricks 的这篇文章获得灵感,整理了一张速查表,方便根据需要选择合适的方法:
| 方法 | 是否占布局空间 | 是否可被屏幕阅读器访问 | 是否可被键盘聚焦 | 是否适合动画 |
|---|---|---|---|---|
hidden / display: none | 否 | 否 | 否 | 否 |
opacity: 0 | 是 | 是 | 是 | 是 |
visibility: hidden | 是 | 否(一般) | 否 | 是(可动画) |
移出视口(position) | 是(占原位) | 是 | 是 | 是 |
clip-path(裁剪为 0) | 是 | 是 | 是 | 是 |
aria-hidden="true" | 是(仍可见) | 否 | 是(需避免) | 不涉及 |
inert | 是(仍可见) | 否 | 否 | 不涉及 |
当我们需要对“显示/隐藏”做动画(例如展开移动端导航)时,必须以可访问的方式来实现。下面分别看一个不好的例子和一个好的例子。
菜单动画 - 不好的例子
菜单展开时使用划入动画,最简单的写法是这样:
ul {
opacity: 0;
transform: translateX(100%);
transition: 0.3s ease-out;
}
ul.active {
opacity: 1;
transform: translateX(0);
}
菜单在添加 .active 类时显示,否则折叠。这个类通过 JavaScript 添加:
menuToggle.addEventListener('click', function (e) {
e.preventDefault();
navMenu.classList.toggle('active');
});

效果看起来不错,但它有一个大问题:opacity: 0 不会把导航从可访问性树中移除。即便菜单在视觉上隐藏了,它仍然能被键盘聚焦,也仍然能被屏幕阅读器读到。
下面是来自 Chrome DevTools 的可访问性树截图:

下面是 macOS 上的辅助工具 VoiceOver 看到的页面内容,同样能看到隐藏的导航:

简而言之,可访问性树是屏幕阅读器用户能访问的所有内容的集合。在我们的例子里,隐藏的导航列表仍然出现在其中。因此隐藏菜单时需要同时解决两个问题:
- 不能被键盘聚焦;
- 不能被屏幕阅读器访问。
菜单动画 - 好的例子
为了解决上述问题,我们在菜单导航上叠加 visibility: hidden,确保它不仅在视觉上隐藏,屏幕阅读器也无法访问:
ul {
visibility: hidden;
opacity: 0;
transform: translateX(100%);
transition: 0.3s ease-out;
}
ul.active {
visibility: visible;
opacity: 1;
transform: translateX(0);
}
添加后,菜单对屏幕阅读器也隐藏了。再用 VoiceOver 测试:

补充:如果整个隐藏区域包含多个可交互元素,逐个用
visibility: hidden比较繁琐,更现代的做法是给容器加上inert属性(见下文),一次性移除内部所有元素的焦点与交互。
inert 属性(补充)
inert 是一个全局属性,用于把某个区域整体“冻结”:被它修饰的元素及其后代会从 Tab 顺序中移除、无法被聚焦、不响应点击等交互,并从可访问性树中隐藏。它非常适合配合模态框、抽屉导航等场景使用:
<div class="page" inert>
<!-- 当模态框打开时,整页背景内容被冻结 -->
</div>
<dialog open>...</dialog>
// 打开模态框时
pageEl.inert = true
// 关闭时
pageEl.inert = false
相比手动给每个元素加 aria-hidden、tabindex="-1"、pointer-events: none,inert 用一行就能完成,而且语义更准确。主流浏览器已普遍支持。
自定义复选框(sr-only)

原生复选框很难自定义样式。常见的做法是隐藏真正的 <input>,用 <label> 模拟外观:
<p class="c-checkbox">
<input class="sr-only" type="checkbox" name="" id="c1">
<label class="c-checkbox__label" for="c1">Custom checkbox</label>
</p>
为了让 <input> 在视觉上消失但保留键盘和屏幕阅读器的可访问性,需要一个俗称 sr-only(screen-reader-only)或 visually-hidden 的工具类。它通过“绝对定位 + 裁剪 + 1px 尺寸”把元素压缩成一个看不见的点:
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0 0 0 0);
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
这样既隐藏了原生外观,又能正常访问自定义复选框。.sr-only:focus / .sr-only:focus-visible 时还可以把它恢复显示,方便键盘用户看到焦点。更多细节可以参考这篇文章。
对屏幕阅读器隐藏内容

在标题里用了一个表情符号。如果不加处理,屏幕阅读器会这样朗读:
Hiding On The Web grinning face with open mouth
==每个表情符号都有一个对应的描述,屏幕阅读器朗读时会使用它。想象你正在浏览网页,突然听到这样的标题,读到后面可能就有点懵了。为了避免这种困扰,可以用 aria-hidden 把表情符号设为对屏幕阅读器隐藏==:
<h1>Hiding On The Web <span aria-hidden="true">😃</span></h1>
小改变,大胜利!
隐藏按钮

Twitter 上有一个名为“See New Tweets”的按钮,默认用 aria-hidden 对屏幕阅读器隐藏,只在有新推文可见时才显示出来。
隐藏修饰性内容

用户 ID 和日期之间的分隔点是装饰性的,因此用 aria-hidden="true" 避免被屏幕阅读器读出来。
总结
“隐藏”是一个需要分情况讨论的话题,关键是先想清楚你想达到哪一种效果:
| 目标 | 推荐做法 |
|---|---|
| 完全移除(不可见、不占位、不可访问、不可交互) | hidden 属性 或 display: none |
| 仅视觉隐藏,仍可访问、可交互 | opacity: 0(配合 pointer-events: none) |
| 视觉隐藏、不可访问、不可聚焦,但保留布局空间 | visibility: hidden |
| 仅对屏幕阅读器隐藏,视觉仍可见 | aria-hidden="true"(且元素本身不可聚焦) |
| 整块区域冻结(不可聚焦、不可交互、不可访问) | inert 属性 |
| 视觉隐藏但保留可访问性(如自定义控件) | .sr-only / visually-hidden 工具类 |
几条容易踩坑的要点:
opacity: 0和移出视口的元素仍然可被聚焦和访问,做菜单/弹层隐藏时要额外处理;aria-hidden不能用在可聚焦元素上,否则键盘能聚焦、屏幕阅读器却读不到;display: none不是可靠的“阻止图片加载”手段,行为随浏览器和loading属性变化;- 需要“显示/隐藏”动画时,优先用
visibility+opacity组合,或用inert管理整块交互区域。