上周调试一个自定义弹窗组件,遇到一个特别头疼的问题:弹窗打开后,鼠标确实被遮罩挡住了,但键盘用户按 Tab 键,焦点却还能跑到弹窗后面的按钮上。更离谱的是,如果用屏幕阅读器朗读,它已经把弹窗内容读完之后,还会接着读背景里的文字。
我第一反应就是给背景加个 pointer-events: none 以及 aria-hidden="true"。结果发现 pointer-events 只阻止鼠标,不能阻止键盘焦点;而 aria-hidden 对屏幕阅读器有效,却不能阻止鼠标点击。把这些用了个遍,还是没法彻底让背景“消失”。
这时候我才想起那个一直被忽略的 HTML 属性 — inert。它是专门用来干掉一段交互区域的。给某个元素加上了它,那么该元素及其整个子树都会变得“不存在”,鼠标点击无效,焦点无法进入,屏幕阅读器也不会读出来。简直就是网页里的“黑洞”。
inert 到底改变了什么
inert 是一个布尔属性,只要写了就生效。不需要赋值为 true,直接这样写就行:
<main inert>
这里面的东西既不能点也不能选,键盘也进不来
</main>
浏览器会把这个区域从可交互树里彻底移掉。所有可聚焦元素(按钮、链接、输入框)都不可聚焦,所有文本都不能被选中,连鼠标滚轮滚动也可能被禁用。这可比单纯设置一个 disabled 全面得多。
最值得一提的是,disabled 只能作用于表单控件,而且不能影响它里面的链接或自定义组件。inert 一次性把内部所有可能产生交互的东西全部屏蔽。
不是所有看着像“隐藏”的属性都能替代它
很多人都试过用 aria-hidden 配一下,但是 aria-hidden 只会让内容从辅助技术中移除,视觉和鼠标交互依然保留。同理,visibility: hidden 虽然看不见也不能点,但它会让元素完全占据不了空间,布局会跳动。 opacity: 0 加 pointer-events: none 更是无法防键盘。
只有 inert,能做到视觉保留、但响应式全部“物理隔离”。这在模态框场景中是一个惊人的好帮手。
常见示例:弹窗打开时锁定背景
假设我们写了下面这种一个简易弹窗结构:
<body>
<header>
<button id="openPopup">打开设置弹窗</button>
</header>
<main id="pageMain">
<p>这是一些页面文字</p>
<button>这个按钮绝对不能通过点击触发</button>
</main>
<div id="customPopup" role="dialog" aria-modal="true" hidden>
<p>这里是弹窗内容</p>
<input type="text" placeholder="输入你的昵称" />
<button id="closePopup">关闭</button>
</div>
</body>
在弹窗打开时,我们只想让弹窗内部可交互,而页面里的 <main> 和 <header> 里的按钮都必须禁止操作。此时就可以加 inert:
const pageMain = document.getElementById('pageMain');
const pageHeader = document.querySelector('header');
function openDialog() {
// 把背景区域标记为 inert,同时去掉弹窗的隐藏属性
pageMain.setAttribute('inert', '');
pageHeader.setAttribute('inert', '');
document.getElementById('customPopup').hidden = false;
}
function closeDialog() {
pageMain.removeAttribute('inert');
pageHeader.removeAttribute('inert');
document.getElementById('customPopup').hidden = true;
}
这样即便你没有写任何额外的遮罩层,用户在弹窗中按 Tab,焦点也只能在弹窗内部移动。鼠标想点击背景,也根本点不到,因为背景元素 in ert 后无法作为鼠标事件的目标。
注意:页面里通常有多个兄弟元素,如果你把 body 直接设置为 inert,那么弹窗在 body 内,也会被一并禁掉。所以正确的做法是把弹窗外面的、与弹窗同级的元素设置为 inert。如果弹窗直接放在 body 下,那就选中除它之外的其他主要区块加 inert。 用 JavaScript 管理其实是比较轻松的。
高级技巧:使用代理元素配合原生 dialog
有了原生 <dialog> 后,页面里的其他部分本来会被自动锁定。但在某些特殊浏览器或性能要求下,你可能还是想用普通 div 模仿弹窗。此时就可以把 inert 和原生 dialog 的 showModal() 结合,做一个“轻量级”的降级方案。
例如:优先使用原生 dialog 的模态能力,如果不支持,则准备 fallback:
if (typeof HTMLDialogElement !== 'undefined' && dialog.showModal) {
dialog.showModal();
} else {
// 用 inert 锁定背景
backdropNodes.forEach(el => el.setAttribute('inert', ''));
dialog.hidden = false;
}
不过现在浏览器对 dialog 支持已经很完善,一般不会走 fallback。但如果你是封装组件库,还是值得把 inert 作为兜底。
inert 的继承性:一旦父级 inert,所有子级都“消失”
这意味着如果你给 <body inert>,那么弹窗内容也全部失灵。你可能碰到过一个现象:给某个容器加了 inert 后,容器里的 input 无法点击,鼠标滚轮也不灵,但容器外一切正常。这就是继承的效果。
继承其实也有好处:让代码更简洁。你想封锁一个区域,只需要给区域最外层一个 inert,不用遍历内部所有按钮。所以锁定布局的时候,不需要锁定多个兄弟元素,如果能够在弹窗外包裹一个外壳,那给外壳加 inert 就够了。
<div id="shell">
<header>...</header>
<main>...</main>
<footer>...</footer>
</div>
<div id="popupContent">...</div>
// 当弹窗打开时,只需给#shell 加 inert 即可
但这样的结构会要求弹窗是 body 下的兄弟节点,如果你的弹窗被渲染在某个父容器内,就得小心处理。
动态添加和移除 inert 时的细节
有人刚开始把 inert 作为一个属性设来设去,但需要注意,不要只依赖 `setAttribute(‘inert’, ‘true’)`,因为有人会误以为需要给 `inert="false"` 来取消。HTML 布尔属性即使写 false 依然代表 true。所以正确删除方式是调用 removeAttribute('inert')。
一旦给元素设置了 inert,其内部所有节点都会失去交互能力。如果你之后想要让里面某个按钮可点,那是不可能的,除非移除整个父级 inert 或者把那个按钮移动到外面。这和 disabled 不一样。
还有一个潜在坑:如果弹窗内容中有需要自动聚焦的输入框,而你在 body 上设置了 inert,那这个输入框可能无法被自动聚焦,因为它在被 ignored 的子树里。所以我们要确保弹窗本身不在被加 inert 的父级里面,或者等移除 inert 后再设置焦点。
实战案例:做一个防穿透的侧边栏
除了弹窗,还有一个更经典的场景——侧边抽屉。例如:页面右侧滑出一条筛选栏,同时主内容区域应该禁止滚动和点击。但是整体布局往往有两个兄弟分区:
<div class="page">
<aside id="sidebar">
<button>筛选</button>
<!-- 一堆筛选项 -->
</aside>
<div class="content">
<button>这个按钮在打开抽屉时不能点</button>
</div>
</div>
假设抽屉是侧翼,当抽屉打开,你会给内容区域加 inert,而不是给侧边栏加。但这里有个问题,侧边栏和内容区域是兄弟,为了只禁用内容区,只需要给内容区加:
contentNode.setAttribute('inert', '');
sidebarNode.removeAttribute('inert');
如果你使用 CSS 动画让抽屉从右边滑入,那也要同步操作 inert,不然用户在抽屉动画期间就可能用 Tab 跳到背景去。
这比把侧边栏做成绝对定位置到 body 下更符合结构语义。
关于浏览器兼容,可以放下心
chrome 和 Edge 从 2021 年就开始支持了,Firefox 从 2023 年已默认开启,Safari 在 15.5 版本也支持了。到了 2025 年,已经没有任何理由不去用它。我甚至建议把 inert 作为无障碍焦点管理的默认方案。
与其他属性的正确分工
现在你应该理解了:
aria-hidden只管辅助技术,用于把装饰性元素对读屏隐藏,但保留可交互区域的风险很小。disabled只用于表单控件,让它无法修改值和提交。pointer-events: none只阻止鼠标相关事件,不影响键盘。inert则一把梭,直接让一块区域从交互地图中抹掉。
在一个完善的模态框中,使用原生 dialog 也会自动做相似的背景抑制,但如果你被迫使用自定义弹窗(例如因为框架限制),那 inert 就是最合适的补丁。
一点尝试后的心得
我给那个自定义弹窗组件加上 inert 以后,十几行以前写的遮罩点击修正代码全删掉了。原本还需要监听背景点击来主动关闭,现在背景根本点不到,连误触都没了。而焦点管理也从手动监听 Tab、手动保存上一个 activeElement 变成了直接利用 inert 把焦点自然限制在弹窗内部。
实际上原生 `
这就是 HTML 内置功能的价值:不一定多花哨,但能给网页带来最基础也最坚固的可访问性。希望这篇小文能让你不再被“点击穿透”困扰,也别再用那些七零八落的补丁填坑了。

