做前端弹窗的老哥估计都有过这种体验:弹窗打开时,鼠标还能点到后面的背景按钮,键盘Tab焦点也能跳到背景里去。尤其是用原生Native Dialog的时候,背景部分根本没法自动禁用。你只能手动在打开弹窗时给背景加个click拦截层,再用JS把背景可聚焦元素全部tabindex=-1,弹窗关掉还要一个个恢复,麻烦得要死。
后来我发现了一个无比冷门但超级好用的HTML属性——inert。把它加到任何容器上,这个容器里所有的点击事件、键盘事件、焦点、辅助技术(比如读屏器)都会直接忽略。这意味着你不需要写任何JS,就能让背景“变哑巴”。这篇文章讲讲我怎么用这个属性救活了一个老弹窗项目。
inert属性到底是啥?
简单说,只要写了inert属性,浏览器就会把这个元素当成不存在一样。它不会响应鼠标点击,不会响应键盘操作,里面的焦点元素(比如按钮、链接、输入框)也都不能被聚焦,甚至屏幕阅读器都会跳过它。最方便的是,这个属性可以作用在一个父容器上,那么它所有的后代元素都会被“隔离”。
用法异常简单,直接加到标签上:
<div id="page" inert>
<button>这个按钮点不了</button>
<a href="#" rel="external nofollow" >这个链接也跳不动</a>
</div>
就这么简单。如果你想让他恢复可交互,只需要把这个属性移除。但在实际场景里,我们不是靠手动增删属性来控制的,而是结合JavaScript在弹窗打开时动态加上,关闭时再移除。
一个实际案例:用inert锁住弹窗背景
我最近重构了一个老后台的“新建任务”弹窗。原来弹窗打开后,点背景右键菜单还能弹出来,隔着弹窗点下面的按钮居然能触发事件,用户都投诉好几次了。这次我直接用inert来解决。
页面结构简写一下:
<div id="app-module">
<!-- 这里是弹窗之前的主体内容 -->
<div id="main-area">
<button id="open-dialog">打开新建任务弹窗</button>
</div>
<!-- 弹窗本身 -->
<dialog id="task-dialog">
<form>
<input type="text" placeholder="任务名称">
<button type="button" id="close-dialog">取消</button>
</form>
</dialog>
</div>
现在,在弹窗打开时,我们要让整个id="main-area"加上inert,弹窗关掉时再让它拿掉。使用原生的dialog.showModal()时,浏览器其实会自动把弹窗外的部分视作不可交互,但我这个弹窗是老式div模拟的,所以必须自己处理。代码很简单:
const dialog = document.getElementById('task-dialog');
const mainArea = document.getElementById('main-area');
document.getElementById('open-dialog').addEventListener('click', () => {
mainArea.setAttribute('inert', '');
dialog.style.display = 'block';
});
document.getElementById('close-dialog').addEventListener('click', () => {
mainArea.removeAttribute('inert');
dialog.style.display = 'none';
});
就这样,背景区域的所有点击都被屏蔽了。如果你用原生dialog.showModal(),其实并不需要这样做,但为了兼容老的div弹窗,这个方案不失为一种救急手段。
inert还能管住焦点
点击只是表面问题,键盘用户的困惑更大。原来弹窗打开后,按Tab键焦点会从弹窗跑到背后的输入框里,用户一脸懵:明明弹窗在眼前,光标怎么跑到后面去了?inert可以帮你把所有背景可聚焦元素“隐形”,Tab永远不会移到那里。
还是刚才那个例子,给main-area加了inert之后,你按Tab键只能在弹窗内部循环。这正好符合“模式对话框”的行为规范。不需要额外写focus-trap库,不用手动管理tabindex,浏览器直接帮你处理好。
注意:inert会继承给所有子元素
但这既是优点也是坑。如果你给一个父级加了inert,那父级里所有东西都不可交互,想单独让某个按钮恢复交互是不可能的。如果你有这种需求,还得把这个按钮移出这个容器。比如我的应用里,弹窗打开时主体区域有个“客服聊天”悬浮球,我希望它在弹窗打开时仍然可以点击(类似微信的悬浮窗)。那就不方便把它放在被加inert的容器里。
解决方法是把那个悬浮球单独放到弹窗外层,也就是不受inert影响的地方。如果没有这种例外,用起来还是很舒服的。
inert的另一个用途:防止点击穿透
一些前端动画需要在点击遮罩层时关闭弹窗,但点击弹窗内部时不关闭。如果用inert把背景元素禁用了,就不会出现“点击背景时弹窗关掉后又触发了背景按钮”这种尴尬。因为背景已经被inert,根本不接收任何点击,你也不用写一堆stopPropagation。
比如我做侧滑抽屉时,打开抽屉后需要背景不可点,通常是要加一个透明遮罩层。有了inert,连遮罩层都不用写,直接在抽屉打开时给背景加inert,点击抽屉外部区域(此时其实是空白区域)也不会触发任何事件。这给日常开发省了不少事。
它和pointer-events:none有什么区别?
很多人第一次听到inert会说:那不就是`pointer-events: none`吗?区别很大。pointer-events:none只禁掉了鼠标点击事件,键盘用户依然可以Tab到背景中的按钮,屏幕阅读器也照样能读到背景内容。而且pointer-events:none还有副作用:如果你在那个元素上绑定了一些事件,连事件冒泡都会出问题。
inert是直接让该元素变成一个“不可感知”的区块,鼠标、键盘、辅助技术全部失效,功能上比pointer-events完整多了。所以如果你想要的是“彻底隔离一段区域”,用inert更靠谱。
浏览器支持情况
目前主流浏览器都支持inert了。Chrome 102以上、Firefox 112以上、Safari 15.5以上都支持。可以说现代浏览器基本都能用。如果你的用户还停留在老掉牙的Chrome 80,那就没办法了,只能像老辈那样用JS写一个focus trap。
如果你担心兼容性,可以这样写一个贴心的回退:
if ('inert' in document.createElement('div')) {
mainArea.setAttribute('inert', '');
} else {
// 老浏览器手动设置tabindex=-1
mainArea.querySelectorAll('a, button, input').forEach(el => el.setAttribute('tabindex', '-1'));
}
不过在我看来,现在新项目基本都可以直接用,不用考虑旧浏览器。
还有一个容易被忽略的功能:配合search
最近Chrome还新出了一个hidden="until-found",它和inert有点像,能让元素默认隐藏但能被用户的浏览器查找功能找到。配合inert可以做出更丝滑的“搜索定位”。不过那是另一个话题了,我还没来得及具体实验。如果你感兴趣,可以自己查查。
总结
inert这个属性看起来很简单,但它解决了我长久以来的痛点:弹窗背景的交互隔离。不需要引入任何库,也不需要手写focus-trap,就一个布尔属性,开箱即用。强烈建议你下一次做弹窗或抽屉时试试它,真的省心。
好,今天的实战记录就到这里。如果你也用过inert,或者踩过什么坑,欢迎在评论区聊聊。

