使用现代 CSS 和 JS 创建无障碍弹出框

弹出框能够在不中断用户工作流程的情况下展示上下文信息,但对许多开发者来说,以无障碍的方式实现它们仍然是一个挑战。无论您是在现代化遗留代码还是构建组件库,理解弹出框、工具提示和模态框之间的区别对于创造正确的用户体验至关重要。
本文介绍如何使用现代 CSS 和 JavaScript 构建无障碍弹出框,从动态定位到键盘导航,同时探索能够降低复杂性的原生浏览器 API。
核心要点
- 弹出框显示丰富的交互式内容,会持续显示直到被关闭,不同于工具提示只在悬停时显示简短提示
- 原生 Popover API 消除了 JavaScript 的复杂性,同时提供内置的无障碍功能
- 正确的焦点管理和 ARIA 属性对于键盘导航至关重要
- 动态定位确保弹出框在视口边界内保持可见
理解弹出框 vs. 工具提示 vs. 模态框
工具提示在悬停时提供简短提示,通常包含单行文本。当用户移开光标时它们会消失,不能包含交互元素。
弹出框显示更丰富的内容——标题、段落、按钮或表单。它们会保持可见直到被明确关闭,允许用户与弹出框内容和下方页面进行交互。
模态框通过使背景变为惰性状态来创建聚焦体验。用户必须完成模态框交互后才能返回主要内容。
核心实现要求
视口内的动态定位
现代弹出框必须适应可用的屏幕空间。当弹出框会超出视口边缘时,它应该自动重新定位:
const positionPopover = (trigger, popover) => {
const triggerRect = trigger.getBoundingClientRect()
const popoverRect = popover.getBoundingClientRect()
let top = triggerRect.bottom + 8
let left = triggerRect.left
// 如果下方空间不足则翻转到上方
if (top + popoverRect.height > window.innerHeight) {
top = triggerRect.top - popoverRect.height - 8
popover.classList.add('popover--top')
}
// 调整水平位置
if (left + popoverRect.width > window.innerWidth) {
left = window.innerWidth - popoverRect.width - 16
}
popover.style.top = `${top}px`
popover.style.left = `${left}px`
}
箭头对齐
CSS 处理指向触发元素的视觉箭头:
.popover::after {
content: "";
position: absolute;
width: 12px;
height: 12px;
background: inherit;
border: inherit;
transform: rotate(45deg);
top: -7px;
left: 20px;
border-bottom: 0;
border-right: 0;
}
.popover--top::after {
top: auto;
bottom: -7px;
transform: rotate(225deg);
}
关闭机制
无障碍弹出框需要多种关闭方式:
// 点击外部区域
document.addEventListener('click', (e) => {
if (!popover.contains(e.target) && !trigger.contains(e.target)) {
closePopover()
}
})
// ESC 键
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && isPopoverOpen) {
closePopover()
trigger.focus() // 将焦点返回到触发器
}
})
Discover how at OpenReplay.com.
原生 Popover API
Popover API 消除了大量 JavaScript 复杂性:
<button popovertarget="my-popover">打开信息</button>
<div id="my-popover" popover>
<h3>附加信息</h3>
<p>这个弹出框的基本功能不需要 JavaScript。</p>
<button popovertarget="my-popover">关闭</button>
</div>
这种原生方法自动处理定位、关闭和焦点管理。为了改善无障碍性,可以将其与 <dialog>
元素结合使用:
<dialog id="enhanced-popover" popover>
<h2>无障碍弹出框</h2>
<p>将 dialog 与 popover 结合提供语义含义。</p>
<button popovertarget="enhanced-popover">关闭</button>
</dialog>
比较库解决方案与原生解决方案
传统库如 Popper.js 提供广泛的定位算法,但会为您的包增加 15-30KB。原生 Popover API 提供:
- 基本功能零 JavaScript
- 内置无障碍功能
- 自动焦点管理
- 浏览器优化定位
对于复杂的定位需求,库仍然有价值。对于标准用例,原生解决方案显著降低了复杂性。
基本无障碍考虑事项
ARIA 属性
在不使用原生 API 构建自定义弹出框时:
<button
aria-expanded="false"
aria-controls="custom-popover"
aria-haspopup="dialog">
打开弹出框
</button>
<div
id="custom-popover"
role="dialog"
aria-labelledby="popover-title"
aria-modal="false">
<h2 id="popover-title">弹出框标题</h2>
<!-- 内容 -->
</div>
焦点管理
正确的焦点顺序确保键盘用户能够有效导航:
const focusableElements = popover.querySelectorAll(
'a, button, input, textarea, select, [tabindex]:not([tabindex="-1"])'
)
// 在弹出框内限制焦点
popover.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
const firstElement = focusableElements[0]
const lastElement = focusableElements[focusableElements.length - 1]
if (e.shiftKey && document.activeElement === firstElement) {
e.preventDefault()
lastElement.focus()
} else if (!e.shiftKey && document.activeElement === lastElement) {
e.preventDefault()
firstElement.focus()
}
}
})
防止背景滚动
使用原生 API 时,仅用 CSS 就可以防止背景滚动:
body:has(dialog[popover]:popover-open) {
overflow: hidden;
}
总结
构建无障碍弹出框需要平衡用户需求与技术实现。原生 Popover API 简化了开发过程,同时保持无障碍标准,尽管复杂交互仍然需要自定义解决方案。
专注于键盘导航、正确的 ARIA 实现和清晰的关闭模式。无论使用原生 API 还是构建自定义组件,无障碍性都必须驱动您的实现决策——确保您的弹出框适用于所有用户,无论他们如何与您的界面交互。
常见问题
对于标准实现使用原生 Popover API,因为它提供内置无障碍性且不需要 JavaScript。只有当您需要复杂的定位逻辑或必须支持缺乏原生 API 支持的旧版浏览器时,才选择像 Popper.js 这样的库。
工具提示在悬停时显示简短文本并自动消失,只需要简单的 ARIA 标签。弹出框包含交互元素,需要焦点管理、多种关闭方法和适当的 ARIA 属性,包括 role dialog 和 aria-modal,以确保屏幕阅读器正确播报它们。
Popover API 在 Chrome 114+、Edge 114+ 和 Safari 17+ 中有支持。Firefox 支持正在开发中。在生产环境中实现之前,始终检查当前浏览器兼容性,并使用功能检测为不支持的浏览器提供回退方案。
Understand every bug
Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.