Back

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

使用现代 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() // 将焦点返回到触发器
  }
})

原生 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.

OpenReplay