用 inert 与 dialog 提升模态框可访问性
用 inert 属性在模态框打开时禁用背景交互
问题场景
用 <dialog> 元素配合 showModal() 打开模态框时,浏览器会把对话框提升到顶层,并默认把焦点限制在对话框内。但很多页面会把对话框写在主内容容器内部,或者用自定义 div 模拟模态框,结果按 Tab 键时焦点仍然可能跑到背景的按钮和链接上;屏幕阅读器也能继续读取背景内容。用户容易在模态框未关闭时误点背景操作,造成流程混乱。
inert 属性可以标记一棵 DOM 子树为“惰性”:该子树不可点击、不可聚焦,并从可访问性树中移除。把它用在模态框打开期间的主内容区,可以补上 showModal() 之外的背景交互隔离。
最小可运行示例
以下示例把主内容放在 #main 中,把原生 dialog 放在与 #main 并列的位置。打开时给 #main 设置 inert,关闭时移除,并把焦点还给触发按钮。
<!doctype html>
<html lang='zh-CN'>
<head>
<meta charset='utf-8'>
<title>inert 模态框示例</title>
<style>
body { font-family: sans-serif; padding: 2rem; }
#main { max-width: 40rem; }
dialog {
border: 1px solid #999;
border-radius: 8px;
padding: 1.5rem;
max-width: 28rem;
}
dialog::backdrop { background: rgba(0, 0, 0, .5); }
</style>
</head>
<body>
<main id='main'>
<h1>页面主内容</h1>
<p>模态框打开后,这里会设置 inert,键盘焦点不会逃逸到这里。</p>
<button id='openBtn' type='button'>打开模态框</button>
<a href='https://example.com' target='_blank' rel='noreferrer'>示例链接</a>
</main>
<dialog id='myDialog' aria-labelledby='dialogTitle' aria-describedby='dialogDesc'>
<h2 id='dialogTitle'>确认操作</h2>
<p id='dialogDesc'>请确认是否继续。按 Esc 或点击取消可关闭。</p>
<form method='dialog'>
<button value='cancel' type='submit'>取消</button>
<button value='confirm' type='submit'>确认</button>
</form>
</dialog>
<script>
const main = document.querySelector('#main');
const dialog = document.querySelector('#myDialog');
const openBtn = document.querySelector('#openBtn');
let lastFocused = null;
function openDialog() {
lastFocused = document.activeElement;
main.setAttribute('inert', '');
dialog.showModal();
}
function closeDialog() {
dialog.close();
}
openBtn.addEventListener('click', openDialog);
dialog.addEventListener('close', () => {
main.removeAttribute('inert');
if (lastFocused && typeof lastFocused.focus === 'function' && lastFocused.isConnected) {
lastFocused.focus();
}
});
dialog.addEventListener('cancel', (event) => {
event.preventDefault();
closeDialog();
});
</script>
</body>
</html>
运行方法
把代码保存为 modal-inert.html,用支持 dialog 和 inert 的现代浏览器直接打开。若浏览器限制本地文件的部分行为,可在文件所在目录执行:
python3 -m http.server 8000
然后访问 http://localhost:8000/modal-inert.html。用 Tab 和 Shift+Tab 在页面中移动,观察打开对话框后焦点是否只在对话框的“取消”和“确认”按钮之间循环;按 Esc 关闭后,焦点应回到“打开模态框”按钮。
关键细节与边界
-
dialog 不要放在 inert 容器内。示例中
#main会被设置inert,如果dialog也是#main的子元素,它自身也会变成惰性,按钮点不了、表单也提交不了。正确做法是让dialog与主内容平级,或者打开前把它移动到body下。 -
焦点返回要处理目标消失的情况。记录
document.activeElement后,关闭时先用isConnected判断原元素是否还在文档中;如果已经被移除,可以把焦点交给一个稳定的容器或返回按钮,而不是强行focus()一个脱离文档的节点。 -
不要用
aria-hidden代替inert。对包含活动焦点的元素设置aria-hidden,会导致辅助技术忽略该元素,但键盘焦点还在里面,形成冲突。inert同时处理交互和可访问性树,更适合背景隔离。若必须支持不支持inert的旧环境,需要配合tabindex=-1、事件拦截和aria-hidden做降级,并仔细管理焦点。 -
浏览器支持与特性检测。
inert在现代浏览器中可以直接使用,但项目若需要覆盖较旧的运行时,应在代码中检测:const supportsInert = 'inert' in HTMLElement.prototype;不支持时走降级分支,不要直接假设属性一定生效。
-
滚动锁定是另一件事。
dialog::backdrop能拦住点击背景,但页面滚动条可能仍然可用。若产品要求背景完全静止,可增加 CSS,例如在打开时给body加类,设置overflow: hidden,关闭时移除。 -
嵌套对话框要分层管理。如果从当前对话框再打开第二个对话框,两个对话框都使用
showModal()时,浏览器会把顶层对话框作为焦点范围。此时不建议对下层对话框也设置inert,否则可能影响返回时的焦点恢复;应分别记录每一层的触发元素,并在对应close事件中恢复。 -
服务端渲染与框架场景。在使用前端框架时,
inert可以直接作为布尔属性绑定。以常见模板语法为例,打开状态为isOpen时,把inert绑定到主内容容器,关闭时移除;同时确保对话框节点渲染在 inert 范围之外,避免框架的组件嵌套把对话框包进主内容容器。
小结
showModal() 负责把对话框提升到顶层并建立基础焦点陷阱,inert 负责把背景主内容区从交互和可访问性树中隔离。两者配合,再加上关闭后的焦点返回,可以显著减少键盘焦点逃逸和误操作。落地时重点检查三点:对话框节点是否在 inert 范围之外、焦点返回目标是否仍然有效、旧浏览器是否有降级路径。
- 点赞
- 收藏
- 关注作者
评论(0)