写前端的人每次做抽屉和弹窗都要手写一套 focus trap,今天一个 HTML 属性把它彻底原生化了

写前端的人每次做抽屉和弹窗都要手写一套 focus trap,今天一个 HTML 属性把它彻底原生化了

做过后台管理系统的都踩过这个坑:弹窗一打开,背景居然还能被 Tab 键 focus 进去,用户点一下就穿透了。多年来解决这个问题只能靠自己写一套 focus trap——遍历全页找所有可聚焦元素,监听 Tab/Shift+Tab,循环回来,还要处理 ESC。

这套逻辑写一次 50 行起步,每次换项目还要复制粘贴。

inert 属性把这件事彻底原生化了。

inert 是什么

inert 是 HTML 全局属性,加在任意元素上,这棵 DOM 子树就会被浏览器「冻结」:

  • 不能被 focus(Tab 进去会跳过)
  • 点击事件全部失效
  • 屏幕阅读器完全忽略
  • 文字选择也被禁止

效果等于同时设置了 pointer-events: noneuser-select: nonearia-hidden="true",但不需要你写任何 CSS 和 aria 属性——浏览器直接把这个区域从无障碍树里摘掉。

写一个 modal 的前后对比

以前:手写 focus trap

function trapFocus(el) {
  const focusable = el.querySelectorAll(
    'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
  );
  const first = focusable[0];
  const last = focusable[focusable.length - 1];

  function handleTab(e) {
    if (e.key !== 'Tab') return;
    if (e.shiftKey) {
      if (document.activeElement === first) {
        e.preventDefault();
        last.focus();
      }
    } else {
      if (document.activeElement === last) {
        e.preventDefault();
        first.focus();
      }
    }
  }

  el.addEventListener('keydown', handleTab);
  first?.focus();
}

// 打开弹窗时调用
trapFocus(document.getElementById('modal'));

还要在关闭时清理监听器,还要处理 ESC 关闭,还要管滚动锁……一个弹窗光焦点管理就 80 行。

现在:两行搞定

// 打开弹窗时——把背景 inert 掉
document.getElementById('app-root').inert = true;

// 关闭弹窗时
document.getElementById('app-root').inert = false;

背景被 inert 后,Tab 键永远只在弹窗内部循环,不需要任何事件监听,不需要遍历可聚焦元素,不需要处理边界情况。浏览器自己把这个行为做进了平台层。

配合 dialog 元素更干净

如果你用原生 <dialog>showModal() 打开,浏览器已经自动把背景 inert 掉了,根本不需要你手动处理:

const dialog = document.getElementById('my-dialog');
dialog.showModal(); // 背景自动 inert,ESC 自动关闭,焦点自动限制

但如果你的 UI 框架里弹窗不是 <dialog> 元素(比如 React Portal 抽到 body 外面、或者自己写的 Drawer 组件),手动给背景容器加 inert 就是最简洁的方案——比写 focus trap 函数少 95% 的代码。

实际落地怎么用

推荐在一个「根容器」上加 inert,而不是在每个弹窗上单独处理:

// 打开任意遮罩类组件时
document.getElementById('app').inert = true;

// 关闭时
document.getElementById('app').inert = false;

这个模式对所有「需要锁定背景」的 UI 都适用:弹窗、抽屉、Command Palette、确认删除框、Splash 加载页。

配合 dialog.showModal() 使用时,只有当你的框架把 dialog 渲染在主应用容器内部时才需要手动 inert;大多数框架的 Portal 会把 dialog 移到 body 下,这时背景本身就不在你的主容器里了,showModal() 的隐式 inert 已经够用。

浏览器支持

inert 在 2023 年 4 月达到 Baseline Newly Available,Chrome 102+、Firefox 112+、Safari 15.5+ 均已支持。到 2025 年 10 月已达到 Baseline Widely Available,全球兼容性基本没有障碍。

对于还在用旧版 Firefox 的极少数用户,可以用 wicg-inert polyfill,但 2026 年这个 polyfill 已经基本是历史遗留了——主流产品不需要再接。

总结

过去做无障碍弹窗,焦点管理是一个独立的技术债——每个项目都要写,每个前端工程师都要踩坑。

今天只需要两行 JS:

background.inert = true;   // 打开弹窗
background.inert = false;  // 关闭弹窗

焦点自动锁住,屏幕阅读器自动忽略,Tab 键自动循环。那套 50 行的 focus trap 函数,终于可以删了。

评论区

0 条评论

登录后可评论。