如果你写过一个“假模态框” —— 就是用div模拟的那种弹窗,那你一定遇到过这个烦心事:弹窗打开了,背后的按钮还是能用Tab键聚焦,甚至用回车触发点击事件。特别是那些做后台管理的老项目,整个页面全是button和a标签,弹窗一亮相,背景区域的各种控件依然“活跃”得不合时宜。过去我会写一堆JS去手动给背景元素设置tabindex=-1、监听focusin重新拉回焦点,反反复复调试。直到我最近试用了一下HTML的 inert 属性,才发现我原来跟无头苍蝇似的做了两年傻事。
inert是什么?
核心就是一个布尔属性,加在某个元素身上,表示这个元素及其所有子元素都“不可交互”。具体来说:
- 鼠标点击无效,按钮按下没反应
- 键盘Tab不会聚焦到该区域内的任何元素
- 辅助技术(读屏器)也会忽略这块区域
- 元素本身不会响应焦点,连文本选择都不行
这看起来像把整个区域“冻结”了。但它和display:none不一样,元素依然占据页面空间,依然可见,只是没法操作。
基本用法:像开关灯一样简单
直接写在标签上:
<div inert>
<button>这个按钮点不动</button>
<a href="https://example.com" rel="external nofollow" >这个链接也摸不到</a>
</div>
浏览器会帮你屏蔽所有交互事件。但是光这样写死没有太大意义,实际开发中我们要动态地切换这个属性。所以常规用法是配合JS:
// 获取背景区域
const main = document.getElementById('main');
// 禁用
main.inert = true;
// 重新启用
main.inert = false;
没错,它就是一个普通的DOM属性,你可以随时设置。
实战:给一个后台弹窗加上焦点圈禁
我做一个常见的商品列表页面。点击“详情”按钮后,右边滑出一个展示面板,此时我希望面板后面的整个列表区域不可操作。代码如下:
<body>
<div id="backdrop" style="display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.5);"></div>
<aside id="detailPanel" style="position: fixed; right: 0; top: 0; width: 300px; background: white;">
<h2>商品详情</h2>
<p>这里显示商品信息...</p>
<button id="closePanel">关闭</button>
</aside>
<main id="mainList">
<ul>
<li>商品1 <button class="view-detail" data-id="1">查看详情</button></li>
<li>商品2 <button class="view-detail" data-id="2">查看详情</button></li>
</ul>
</main>
<script>
const main = document.getElementById('mainList');
const panel = document.getElementById('detailPanel');
const backdrop = document.getElementById('backdrop');
const closeBtn = document.getElementById('closePanel');
document.querySelectorAll('.view-detail').forEach(btn => {
btn.addEventListener('click', () => {
// 禁用背景区域
main.inert = true;
// 显示遮罩和面板
backdrop.style.display = 'block';
panel.style.display = 'block';
// 焦点移到面板内(虽然inert不会移动焦点,但最好还是主动拉过去)
closeBtn.focus();
});
});
function close() {
main.inert = false;
backdrop.style.display = 'none';
panel.style.display = 'none';
}
closeBtn.addEventListener('click', close);
backdrop.addEventListener('click', close);
</script>
</body>
这样设置之后,当面板打开,mainList 里的所有按钮、链接都不能被Tab聚焦,点击更是无效。用户只能在面板和遮罩之间交互。关掉面板后,inert=false,一切恢复正常。你会发现你根本不需要在背景每个元素上设置tabindex=-1,也不用监听focusout事件去判断焦点是否落在了背景区域,只要一个属性,全部搞定。
结合:has()或状态类实现纯CSS切换?
既然之前写过CSS的:has(),有人可能会问:能不能不写JS,直接用css让inert属性生效?不行,因为inert是HTML属性,不是CSS属性。单纯靠CSS是没法让一个元素变得“焦点不可达”的,尽管你可以用pointer-events: none去禁止鼠标点击,但对键盘焦点毫无作用。所以还是得踏实写两行JS去切换inert。
不过如果你用Vue或React,可以直接在模板里绑定::inert="isOpen",比如Vue里:
<main :inert="isPanelOpen">...</main>
这就更简洁了,框架会帮你更新属性。
与 aria-hidden 有什么区别?
很多人容易搞混。简单说:
aria-hidden="true"只是告诉屏幕阅读器“忽略这块内容”,但Tab焦点依然可以进入。你如果在前台弹窗的背景区块上只设置aria-hidden,键盘用户照样能Tab到背后的按钮上去,体验很糟糕。inert则是真正移出焦点链和交互事件,鼠标键盘全部屏蔽,不管你是读屏器还是普通用户,都无法操作它。
在模态框场景中,最佳组合是:给背景设置inert,同时把面板区域加上role="dialog"和aria-modal="true"。这样对辅助技术也完整。
一个额外的使用场景:侧边栏导航
后台系统里有一种抽屉式导航,平时隐藏,点击汉堡菜单后从左滑出,此时内容区应该禁止操作,让用户专注在导航上。这个需求我以前写过一个带遮罩和监听scroll的组件,可麻烦。现在可以直接在内容区加上inert:
// 当导航打开
document.getElementById('page-content').inert = true;
// 关闭后
document.getElementById('page-content').inert = false;
清爽利索。如果有人用Tab扫描页面,焦点会在导航和侧栏之间循环,不会莫名其妙跑到背后去。
注意:inert不能用于自身是焦点目标的元素
如果你要给某个button加inert,那这个button本身是不可聚焦的,所以它的onclick不会触发。但是,它内部如果有子元素(比如span),span也不会被点击到。这是正常的。但有一种情况比较反直觉:如果你对一个拥有contenteditable的div设置inert,那么文本编辑也不能再进行了。我实际用的时候,发现输入框无法输入,这其实正是“禁用”的语义。
支持程度怎么样?
目前Chrome 102+、Firefox 112+、Safari 15.5+都支持了。截至2025年,所有主流浏览器都能用。如果你要兼容IE或者旧版移动浏览器,可以引入一个简单的polyfill,不过现在基本没有这个必要了。
如果不确定,可以写一行判断:
if ('inert' in HTMLElement.prototype) {
// 浏览器支持
} else {
// 降级方案
}
实际上,我连判断都没做,因为公司浏览器都已是新版。
几个容易忽略的细节
inert属性生效时,元素的内容也不可选中,所以用户不能复制里面的文字。这个要考虑到,有些场景可能不需要禁止文本复制,那你可以不用inert,改用pointer-events:none加tabindex="-1"组合。- 当某个元素设置为inert后,它的所有后代都被视为不可交互,哪怕后代上有onclick事件也白搭。
- 如果父元素是inert,但你希望某个子元素可交互,没有办法“解开”子元素。inert不能被子元素覆盖,只能移除父元素的inert属性。
最后的最后,我改写了公司老代码
我把原来一个用iframe遮罩的老弹窗组件重构了一下,用上了inert。代码量从原来的两百多行缩到了几十行。当然这不是唯一原因,但inert确实帮我省掉了大量焦点管理逻辑。以后再有人问“为什么弹窗后面的按钮还能点”,我就会告诉他:“你试试给背景加个inert属性。” 就这么简单。
如果你也在写弹窗或侧栏,强烈建议用inert试一下,相信你也会把那一堆tabindex和focusout删掉的。

