12k
All articles

使用 Inert 属性管理焦点与交互性

使用 inert 属性隔离模态框、抽屉和加载遮罩,阻止焦点、点击以及对可访问性树的访问。

OpenReplay Team
OpenReplay Team
使用 Inert 属性管理焦点与交互性

在元素上设置 inert 会将其整个子树从 Tab 键顺序中移除,阻止所有指针和点击事件,并将其从无障碍树中隐藏,使屏幕阅读器无法发现或播报它——这三种行为由 WHATWG HTML 现行标准强制规定。此外,当前浏览器还会阻止页内查找(Ctrl/Cmd+F)匹配该子树中的文本,并禁用其中的文本选择功能;这些行为虽由规范留给用户代理自行决定,但 MDN 将其记录为当前浏览器的实际实现规范。也就是说,仅凭一个属性就能禁用六个交互通道。

本文旨在解决一个具体问题:当模态框、抽屉或侧边导航打开时,如何干净利落地禁用背景内容——既无需手动编写在移动端屏幕阅读器上容易出错的焦点陷阱,也无需在 DOM 中到处散布 aria-hidden。文章将介绍 inert 阻断的内容、应用它的 HTML 和 JavaScript 语法、一个包含焦点恢复功能的完整模态框示例、如何为 inert 内容添加样式,以及何时应改用 disabledaria-hiddenhidden

核心要点

  • inert 通过一条声明阻断六个交互通道:焦点、指针/点击事件、Tab 键顺序和无障碍树可发现性(均为规范强制要求),以及页内查找和文本选择(用户代理自行决定,但已在当前浏览器中普遍实现)。
  • inert 于 2023 年 4 月成为 Baseline 新可用特性——Chrome 和 Edge 102、Firefox 112、Safari 15.5 均已支持——并于 2025 年 10 月前后达到 Baseline 广泛可用状态,这使得 wicg-inert polyfill 成为历史遗留方案,而非生产环境的必要依赖。
  • 思维模式的转变在于”守卫”与”陷阱”的区别:焦点陷阱通过 JavaScript 将用户锁定在组件内部;而 inert 则守卫页面其余部分,让浏览器原生地强制执行边界。
  • 除 HTML 布尔属性外,inert 还以 HTMLElement.inert IDL 属性的形式暴露,这是一个可在 JavaScript 中设置的布尔值——打开时执行 mainEl.inert = true,关闭时执行 mainEl.inert = false
  • <dialog>.showModal() 会自动将页面其余部分设为 inert,因此手动管理 inert 仅在使用原生元素之外构建自定义对话框模式时才有必要。

inert 属性阻断的内容

inert 是一个 HTML 全局属性,可使元素及其整个子树变为不可交互且不可被发现的状态。根据 WHATWG HTML 现行标准的 inert 子树章节,处于 inert 状态的节点不会接收点击和焦点等用户交互事件,用户代理必须将其从无障碍树中排除。以下三项阻断是规范性的,在所有实现中保持一致:

  1. 焦点 —— inert 元素无法通过 Tab 键、点击或编程方式 element.focus() 获得焦点。
  2. 指针和点击事件 —— 用户发起的点击和指针事件不会到达 inert 节点。
  3. 无障碍树可发现性 —— 辅助技术无法找到或播报该子树。

规范中还有另外两项阻断留给用户代理自行决定(使用规范性的 may 措辞),但 MDN 将其记录为当前浏览器的实际实现行为:

  1. 页内查找 —— Ctrl/Cmd+F 不会匹配 inert 子树内的文本。
  2. 文本选择 —— 用户无法选择 inert 文本。

Tab 键顺序是焦点阻断的自然结果:由于 inert 节点无法获得焦点,它们会被完全从顺序焦点导航中移除。有一个重要边界需要注意:inert 阻断的是用户发起的事件,而非编程式事件。在 inert 子树内调用 dispatchEvent() 或触发计时器回调仍会正常执行——inert 不是 alert(),不会冻结 JavaScript 的执行。

需要牢记的陷阱:由于 inert 会将子树从无障碍树中移除,切勿将其应用于用户仍需阅读的内容。如果你只需要在视觉上隐藏某些内容同时保持其可被发现,则需要使用其他工具。

两种典型使用场景

inert 适用于两种情况,均记录在 web.dev 的 inert 指南中:存在于 DOM 中但位于屏幕外或被隐藏的 DOM,以及可见但不应具有交互性的 DOM。

屏幕外或隐藏的 DOM。 滑出式导航抽屉或侧边导航在可见之前就已将可聚焦链接添加到 DOM 中。若不使用 inert,键盘用户可以通过 Tab 键进入已关闭的抽屉,并聚焦到他们看不见的控件上。在抽屉打开之前将其容器标记为 inert,可使这些链接保持在 Tab 键顺序之外:

<nav id="drawer" inert>
  <a href="/dashboard">Dashboard</a>
  <a href="/settings">Settings</a>
</nav>

可见但不可交互的 UI。 当表单正在提交、页面正在加载,或模态遮罩层覆盖在背景内容之上时,该内容虽然可见,但不应接受输入。在提交期间对表单应用 inert 可防止重复提交和焦点游离:

<form id="signup" inert>
  <!-- 请求进行中时,字段作为一组被禁用 -->
</form>

两种情况遵循相同的逻辑:内容保留在 DOM 中(因此布局、过渡效果和状态得以保留),但浏览器拒绝将交互路由到该内容。

语法:HTML 属性与 HTMLElement.inert 属性

inert 有两种接口:HTML 布尔属性和 HTMLElement.inert IDL 属性。属性用于静态或服务端渲染的标记;属性(property)用于在 JavaScript 中切换状态。

作为布尔属性,其存在本身就是关键——inertinert="" 是等价的,且默认值为 false(不存在即表示可交互):

<main inert>
  <!-- 此处所有内容均不可交互 -->
</main>

要在运行时切换它,请使用 HTMLElement.inert 属性,这是一个可直接读取和设置的布尔值——无需繁琐的 setAttribute / removeAttribute 操作:

const mainEl = document.querySelector('main');

// 禁用与页面其余部分的交互
mainEl.inert = true;

// 恢复交互
mainEl.inert = false;

这是该 API 最简洁的部分,也是大多数现有文章所缺失的内容:打开/关闭切换只需两次赋值。与下文介绍的八步焦点陷阱流程相比,差异显而易见。

inert 出现之前:焦点陷阱及其脆弱性

inert 出现之前,限制模态框范围的标准方式是 JavaScript 焦点陷阱——一种拦截 Tab 和 Shift+Tab 键以将焦点循环限制在对话框内的逻辑。CSS-Tricks 列举的标准流程大约需要八个步骤:找到页面上所有可聚焦元素,确定模态框内第一个和最后一个可聚焦元素,剥夺其外部所有元素的交互性和可发现性,将焦点移入其中,监听关闭事件,关闭时恢复所有内容,并将焦点返回触发元素。

第一步——“找到所有可聚焦元素”——本身就是 bug 的来源,因为原生可聚焦元素的集合比大多数人记忆中的要大。在没有任何 tabindex 的情况下,能够进入顺序 Tab 键顺序的元素包括:

  • 带有 href<a><area>
  • <button><input><select><textarea>(除非设置了 disabled
  • <iframe><embed><object>
  • 带有 controls 属性的 <audio><video>
  • <summary><details> 内的第一个)
  • 任何具有非负 tabindex 的元素,以及任何 contenteditable 元素

在构建陷阱时遗漏其中一个,键盘用户就能逃脱;过度管理列表,则会与浏览器自身的排序产生冲突。更安全的默认做法是保持文档的自然焦点顺序不变,仅在组件确实需要时才进行干预——而这正是 inert 所消除的大部分手动工作。

思维模式的转变正是关键所在。焦点陷阱通过拦截按键将用户锁定在组件内部;而 inert 则通过使对话框外的所有内容不可访问来守卫页面其余部分——边界由浏览器强制执行,而非你的 JavaScript。这种”守卫”与”陷阱”的框架来自 LogRocket 对该属性的分析

手动编写的陷阱在以下三种情况下反复失效:

  • 移动端辅助技术。 Android 上的 TalkBack 和 iOS 上的 VoiceOver 通过滑动手势而非 Tab 键进行导航。仅拦截键盘事件的 JavaScript 陷阱对基于滑动手势的屏幕阅读器用户完全没有边界效果。inert 在平台层面阻断子树,同时覆盖键盘和手势导航。
  • aria-hidden 蔓延。 inert 出现之前的变通方案是对每个非模态元素设置 aria-hidden="true"。在具有深层 DOM 树的页面上,这种方式难以维护且经常不完整。
  • 手动 Tab 循环。 Tab/Shift+Tab 拦截逻辑很脆弱,容易出错,尤其是当模态框的可聚焦内容发生变化时。

对模态框和抽屉实现的会话回放经常会发现,在对话框仍然打开的情况下,焦点事件落在了背景内容上——这是焦点边界不完整的典型表现,而这正是 inert 旨在消除的问题。

上述参考资料重建了完整的 trap-focus.js;这里无需重复。相关对比在于代码行数。焦点陷阱需要数十行事件拦截代码,而 inert 的等效实现如下:

function openModal() {
  mainEl.inert = true;
}
function closeModal() {
  mainEl.inert = false;
}

包含焦点恢复功能的模态框完整示例

最简洁的自定义模态框模式是将对话框作为 <main inert> 的兄弟元素放置:模态框位于 inert 子树之外,因此保持可交互状态,而 <main> 中的所有内容则被封锁。这种 <main inert> 兄弟元素模式遵循 CSS-Tricks 记录的结构。以下示例添加了每篇参考资料都遗漏的关键内容——在打开时将焦点移入对话框,并在关闭时将其恢复到触发元素。

<button id="open-modal" type="button">Save changes…</button>

<div
  id="modal"
  class="modal"
  role="dialog"
  aria-labelledby="modal-title"
  aria-modal="true"
  hidden
>
  <h2 id="modal-title">Save changes?</h2>
  <p>Your unsaved changes will be lost.</p>
  <button id="save" type="button" autofocus>Save</button>
  <button id="cancel" type="button">Discard</button>
</div>

<main id="page">
  <!-- 所有页面内容 -->
</main>
const triggerEl = document.getElementById('open-modal');
const modalEl = document.getElementById('modal');
const mainEl = document.getElementById('page');
const cancelEl = document.getElementById('cancel');

let lastFocused = null;

function openModal() {
  lastFocused = document.activeElement;   // 记住触发元素
  modalEl.hidden = false;
  mainEl.inert = true;                    // 守卫页面其余部分
  // 将焦点移至对话框的主要操作
  modalEl.querySelector('[autofocus]').focus();
}

function closeModal() {
  mainEl.inert = false;                   // 恢复页面
  modalEl.hidden = true;
  if (lastFocused) lastFocused.focus();   // 将焦点恢复到触发元素
}

triggerEl.addEventListener('click', openModal);
cancelEl.addEventListener('click', closeModal);
document.addEventListener('keydown', (e) => {
  if (e.key === 'Escape' && !modalEl.hidden) closeModal();
});

关于正确性的几点说明。对话框通过其可聚焦子元素隐式使用了 tabindex="-1" 语义;通常情况下,你不需要在任何地方使用正整数 tabindex——正整数会覆盖自然 Tab 键顺序,是已记录在案的反模式。仅当需要以编程方式聚焦非交互式容器时才使用 tabindex="-1",仅对真正具有交互性的自定义元素使用 tabindex="0"。主要操作上的 autofocus 属性是规范推荐的对话框内焦点起始点。此模式适用于 Chrome 102+、Firefox 112+ 和 Safari 15.5+。

为 inert 内容添加样式:没有默认样式

inert 没有默认的视觉效果——浏览器改变的是行为,而非外观,因此除非你添加样式,否则 inert 内容看起来与活动内容完全相同。web.dev 展示的标准模式是针对 [inert] 属性选择器,并结合三个与该属性阻断的交互通道相对应的属性:

[inert],
[inert] * {
  opacity: 0.5;         /* 视觉变暗——表示"非活动"状态 */
  pointer-events: none; /* 抑制悬停/光标提示 */
  user-select: none;    /* 防止文本选择 */
  cursor: default;
}

每个属性都有其存在的意义:opacity 在视觉上传达禁用状态,pointer-events: none 移除悬停状态和光标变化(否则会暗示可交互性),user-select: none 与该属性已应用的文本选择阻断相匹配。行为由 inert 本身强制执行;CSS 的存在是为了让视力正常的用户能够看到浏览器在底层强制执行的边界。

在 inert、disabled、aria-hidden、hidden 和 pointer-events 之间做选择

根据作用范围和无障碍树行为来选择工具:inert 阻断整个子树的所有交互通道并将其从无障碍树中移除;disabled 阻断单个控件但保持其可被发现;aria-hidden 对辅助技术隐藏内容,同时保留点击和焦点功能;hidden 完全移除内容;CSS pointer-events: none 仅阻断鼠标操作。当需要封锁模态框、抽屉或加载遮罩后面的背景内容时,请使用 inert

工具阻断交互在无障碍树中?作用范围使用时机
inert是(焦点、指针、查找、选择)否——已移除元素 + 子树封锁模态框、抽屉或加载遮罩后面的背景内容
disabled是(针对该控件)是——播报为不可用表单控件或 fieldset 组暂时不可操作的单个按钮、输入框或表单区域
aria-hidden="true"否——点击/焦点仍有效否——已移除元素 + 子树仅对辅助技术隐藏装饰性或重复内容
hidden / display:none是——完全移除否——未渲染元素 + 子树当前不应在视觉上或对辅助技术存在的内容
pointer-events: none仅鼠标——键盘/辅助技术不受影响元素 + 子树装饰性点击穿透;绝不能替代 inert

两个常见错误:对背景内容使用 aria-hidden 同时保留其可点击和可聚焦状态(它仍在 Tab 键顺序中),以及使用 pointer-events: none 并假设键盘和屏幕阅读器用户也被阻断(实际上并非如此)。要完全封锁背景,inert 是唯一能覆盖所有通道的单一工具。

使用 dialog.showModal() 时还需要 inert 吗?

当你使用 HTMLDialogElement.showModal() 打开对话框时,浏览器会自动将页面其余部分设为 inert——顶层行为包含一个隐式的 inert 边界,因此对话框外的所有内容都会变得不可点击和不可 Tab 键访问,无需你进行任何属性管理。手动 inert 仅在你使用原生 <dialog> 元素之外构建自定义对话框模式时才有必要,如上述完整示例所示。

<dialog id="confirm">
  <p>Delete this item?</p>
  <button>Delete</button>
  <button>Cancel</button>
</dialog>
document.getElementById('confirm').showModal(); // 页面自动设为 inert

如果可以使用带有 showModal()<dialog>,你可以免费获得 inert 边界。当辅助技术支持方面的顾虑或设计约束迫使你使用自定义对话框时,再考虑手动使用 inert

浏览器支持与新兴的 CSS interactivity 属性

inert 于 2023 年 4 月成为 Baseline 新可用特性——Chrome 和 Edge 在版本 102 中支持,Firefox 在 112 中支持,Safari 在 15.5 中支持——并于 2025 年 10 月前后达到 Baseline 广泛可用状态(距互操作日期约 30 个月)。wicg-inert polyfill 现已成为历史遗留方案,而非生产环境的必要依赖;其最后一个版本为 v3.1.3(2023 年),且已不再积极维护。其 README 还指出,该 polyfill”在性能方面代价高昂”,因为它”需要大量的树遍历”——而原生实现避免了这一代价。对于 2023 年以后发布的任何浏览器,你都不再需要它。

一个较新的基于 CSS 的替代方案是 interactivity 属性,它接受 interactivity: inert 来通过样式表而非属性应用 inert 行为。这是一个新兴特性,支持范围较窄:根据 caniuse 数据,截至 2026 年中期,它仅支持 Chromium(Chrome/Edge 135+,2025 年 3 月),Firefox 和 Safari 尚未支持,且不属于 Baseline(有限可用)。将其视为仅适用于 Chromium 环境的前瞻性选项,而非该属性的跨浏览器替代方案。

结论

对于封锁模态框、抽屉或加载遮罩后面的背景内容,inert 用一个属性取代了手动焦点陷阱和 aria-hidden 蔓延的整套脆弱机制,浏览器会在键盘、指针和辅助技术导航层面统一强制执行这一边界。审查你现有的对话框:如果键盘用户可以在模态框打开时通过 Tab 键进入其后面的页面,请将背景内容——或你的 <main>——包裹在 inert 中,在打开和关闭时用 element.inert 切换它,并将焦点恢复到触发元素。随着该属性自 2025 年起广泛可用,唯一剩下的决策是原生 <dialog> 是否已为你免费提供了这个边界。

常见问题

inert 属性和 aria-hidden 有什么区别?

inert 属性会阻断交互并将内容从无障碍树中移除,使子树既不可访问也不可被发现。aria-hidden 属性仅将内容从无障碍树中移除;它不阻断点击、焦点或键盘交互。对背景内容应用 aria-hidden 同时保留其可点击和可聚焦状态是一个常见错误,因为这些元素仍然保留在 Tab 键顺序中。当需要阻断整个子树的交互时,请使用 inert。

inert 会阻断 JavaScript 事件监听器和编程式事件吗?

不会。inert 属性阻断用户发起的事件,如点击、焦点和指针交互,但不会阻止编程式事件。在 inert 子树内调用 dispatchEvent、执行计时器回调或运行任何脚本仍会正常执行。与 alert 函数不同,inert 不会冻结 JavaScript 的执行;它只改变浏览器将用户交互和无障碍发现路由到被标记子树的方式。

我还需要为 inert 使用 JavaScript polyfill 吗?

对于 2023 年以后发布的任何浏览器,不再需要。inert 属性于 2023 年 4 月成为 Baseline 新可用特性,Chrome 和 Edge 102、Firefox 112 以及 Safari 15.5 均已支持,并于 2025 年 10 月前后达到 Baseline 广泛可用状态。wicg-inert polyfill 现已成为历史遗留方案,而非生产环境的必要依赖;其最后一个版本为 2023 年发布的 v3.1.3,且已不再积极维护。其 README 还指出,该 polyfill 在性能方面代价高昂,因为它需要原生实现所避免的树遍历操作。

为什么传统焦点陷阱在移动端屏幕阅读器上会失效?

传统焦点陷阱在移动端失效,是因为 Android 上的 TalkBack 和 iOS 上的 VoiceOver 通过滑动手势而非 Tab 键进行导航。仅拦截键盘事件的 JavaScript 陷阱对基于滑动手势的屏幕阅读器用户没有任何边界效果,因此他们可以从对话框逃脱到背景内容中。inert 属性在平台层面阻断子树,同时覆盖键盘导航和手势导航,这正是它能够替代手动焦点陷阱逻辑来限制模态框范围的原因。

Digital experience platform

Truly understand users experience

See every user interaction, feel every frustration and track all hesitations with OpenReplay — the open-source digital experience platform. It can be self-hosted in minutes, giving you complete control over your customer data.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.