popover 弹出层
popover 是一个全局属性,用于将元素声明为弹出层(popover),可以通过声明式或编程方式控制其显示和隐藏。弹出层是浏览器原生实现的轻量级弹出内容机制,无需 JavaScript 即可实现基本的弹出交互,同时提供了完善的无障碍支持。
前置知识
阅读本节前,建议先了解:slot 插槽
基础概念
什么是 popover
popover 属性(有时称为弹出 API 或 Popover API)是浏览器原生支持的弹出层功能,它可以替代许多需要 JavaScript 和 ARIA 实现的弹出模式(如工具提示、下拉菜单、通知等)。
html
<!-- 基本用法 -->
<button popovertarget="my-popover">显示弹出层</button>
<div id="my-popover" popover>
<p>这是弹出层内容</p>
</div>popover 的值
| 值 | 说明 | 关闭方式 |
|---|---|---|
auto(默认) | 点击外部区域、按 ESC 键可关闭(轻量级关闭) | 点击外部、ESC、其他弹出层打开 |
manual | 只能通过编程方式关闭 | 必须 JS 关闭 |
弹出层与传统方案对比
| 特性 | popover | dialog (showModal) | 自定义 div + JS |
|---|---|---|---|
| 原生支持 | 是 | 是 | 否 |
| 点击外部关闭 | auto 模式支持 | 不自动 | 需手动实现 |
| ESC 关闭 | 支持 | 支持 | 需手动实现 |
| 焦点陷阱 | 无 | 有 | 需手动实现 |
| 无障碍 | 内置 | 内置 | 需手动实现 |
| 顶层显示 | 是(top layer) | 是(top layer) | 需手动实现 z-index |
| 轻量级 | 是 | 较重 | 视实现而定 |
语法
声明式弹出层
html
<!-- 按钮控制弹出层 -->
<button popovertarget="info-popup">显示信息</button>
<button popovertarget="info-popup" popovertargetaction="hide">隐藏信息</button>
<!-- 弹出层元素 -->
<div id="info-popup" popover="auto">
<h3>信息提示</h3>
<p>这是一个原生弹出层。</p>
<button popovertarget="info-popup" popovertargetaction="hide">关闭</button>
</div>popovertarget 相关属性
| 属性 | 使用位置 | 说明 |
|---|---|---|
popovertarget | 按钮/控件 | 指定要控制的弹出层元素的 ID |
popovertargetaction | 按钮/控件 | 动作类型:show、hide、toggle(默认) |
编程式弹出层
javascript
const popover = document.getElementById('my-popover');
// 显示弹出层
popover.showPopover();
// 隐藏弹出层
popover.hidePopover();
// 切换显示状态
popover.togglePopover();
// 检查是否显示
console.log(popover.matches(':popover-open')); // true/false详细说明
Top Layer 层叠
弹出层自动显示在顶层(Top Layer)中,这意味着它会显示在页面所有其他内容之上,不受 z-index 影响:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Popover Top Layer</title>
<style>
/* 即使有高 z-index 的元素,popover 也会显示在之上 */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
}
.modal-content {
background: white;
padding: 40px;
border-radius: 12px;
z-index: 10000;
}
/* 弹出层样式 */
[popover] {
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 16px;
background: white;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.15);
/* 不需要 z-index,自动在最顶层 */
}
/* 弹出层进入/退出动画 */
[popover] {
transition: opacity 0.25s ease, transform 0.25s ease;
opacity: 0;
transform: translateY(-10px);
}
[popover]:popover-open {
opacity: 1;
transform: translateY(0);
}
/* 开始退出动画 */
@starting-style {
[popover]:popover-open {
opacity: 0;
transform: translateY(-10px);
}
}
</style>
</head>
<body>
<div class="modal-overlay">
<div class="modal-content">
<h2>模态框内容</h2>
<button popovertarget="top-tip" popovertargetaction="toggle">
显示提示
</button>
</div>
</div>
<!-- 这个弹出层会显示在模态框之上 -->
<div id="top-tip" popover>
<p>我在模态框的上方,因为我在 Top Layer 中。</p>
</div>
</body>
</html>auto 与 manual 模式
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>auto vs manual popover</title>
<style>
[popover] {
padding: 16px;
border: 1px solid #e2e8f0;
border-radius: 8px;
background: white;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
}
</style>
</head>
<body>
<!-- auto 模式:点击外部自动关闭 -->
<button popovertarget="auto-popup">显示 auto 弹出层</button>
<div id="auto-popup" popover="auto">
<p>点击页面其他区域或按 ESC 关闭。</p>
</div>
<!-- manual 模式:只能通过按钮关闭 -->
<button popovertarget="manual-popup">显示 manual 弹出层</button>
<div id="manual-popup" popover="manual">
<p>必须点击关闭按钮才能关闭。</p>
<button popovertarget="manual-popup" popovertargetaction="hide">关闭</button>
</div>
</body>
</html>事件监听
javascript
const popover = document.getElementById('my-popover');
// beforetoggle:在显示/隐藏之前触发
popover.addEventListener('beforetoggle', (e) => {
if (e.newState === 'open') {
console.log('弹出层即将打开');
} else if (e.newState === 'closed') {
console.log('弹出层即将关闭');
}
});
// toggle:在显示/隐藏之后触发
popover.addEventListener('toggle', (e) => {
if (e.newState === 'open') {
console.log('弹出层已打开');
// 聚焦弹出层内的第一个交互元素
const firstFocusable = popover.querySelector('button, input, [tabindex]');
if (firstFocusable) firstFocusable.focus();
}
});::backdrop 伪元素
popover="manual" 模式下可以使用 ::backdrop 创建背景遮罩:
css
/* 弹出层背景遮罩(仅 manual 模式有视觉效果) */
[popover]::backdrop {
background: rgba(0, 0, 0, 0.3);
}
/* 也可以为 auto 模式添加 backdrop */
[popover="auto"]::backdrop {
background: rgba(0, 0, 0, 0.1);
backdrop-filter: blur(2px);
}实战示例
完整的下拉菜单
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Popover 下拉菜单</title>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
padding: 40px;
}
.toolbar {
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
background: #f1f5f9;
border-radius: 8px;
}
.menu-btn {
padding: 8px 16px;
border: none;
background: transparent;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
}
.menu-btn:hover {
background: #e2e8f0;
}
[popover] {
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 4px;
background: white;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.15);
min-width: 160px;
}
.menu-item {
display: block;
width: 100%;
padding: 8px 12px;
border: none;
background: none;
text-align: left;
cursor: pointer;
border-radius: 4px;
font-size: 14px;
}
.menu-item:hover {
background: #f1f5f9;
}
.menu-item.danger {
color: #dc2626;
}
.menu-item.danger:hover {
background: #fef2f2;
}
.menu-divider {
height: 1px;
background: #e2e8f0;
margin: 4px 0;
}
/* 进入动画 */
[popover] {
opacity: 0;
transform: scale(0.95);
transition: opacity 0.15s ease, transform 0.15s ease;
transform-origin: top left;
}
[popover]:popover-open {
opacity: 1;
transform: scale(1);
}
@starting-style {
[popover]:popover-open {
opacity: 0;
transform: scale(0.95);
}
}
</style>
</head>
<body>
<div class="toolbar">
<!-- 文件菜单 -->
<button class="menu-btn" popovertarget="file-menu">文件</button>
<div id="file-menu" popover>
<button class="menu-item" popovertarget="file-menu" popovertargetaction="hide">
新建文件
</button>
<button class="menu-item" popovertarget="file-menu" popovertargetaction="hide">
打开文件
</button>
<button class="menu-item" popovertarget="file-menu" popovertargetaction="hide">
保存
</button>
<div class="menu-divider"></div>
<button class="menu-item danger" popovertarget="file-menu" popovertargetaction="hide">
退出
</button>
</div>
<!-- 编辑菜单 -->
<button class="menu-btn" popovertarget="edit-menu">编辑</button>
<div id="edit-menu" popover>
<button class="menu-item" popovertarget="edit-menu" popovertargetaction="hide">
撤销
</button>
<button class="menu-item" popovertarget="edit-menu" popovertargetaction="hide">
重做
</button>
<div class="menu-divider"></div>
<button class="menu-item" popovertarget="edit-menu" popovertargetaction="hide">
全选
</button>
</div>
<!-- 帮助按钮 - 带工具提示 -->
<button class="menu-btn" popovertarget="help-tip">? 帮助</button>
<div id="help-tip" popover>
<p style="margin: 8px; font-size: 13px; color: #64748b;">
使用菜单进行文件和编辑操作。
</p>
</div>
</div>
<script>
// 为所有菜单项添加点击处理
document.querySelectorAll('.menu-item').forEach(item => {
item.addEventListener('click', () => {
const action = item.textContent.trim();
console.log('执行操作:', action);
// 这里可以添加实际的操作逻辑
});
});
</script>
</body>
</html>注意事项
浏览器兼容性
| 浏览器 | 支持版本 |
|---|---|
| Chrome | 114+ |
| Edge | 114+ |
| Safari | 17+ |
| Firefox | 125+ |
对于不支持的浏览器,可以使用 polyfill:
html
<!-- Popover API Polyfill -->
<script src="https://unpkg.com/@oddbird/popover-polyfill/dist/popover-fn.js"></script>嵌套弹出层
html
<!-- 可以嵌套弹出层 -->
<button popovertarget="menu">菜单</button>
<div id="menu" popover>
<button popovertarget="submenu">子菜单</button>
</div>
<div id="submenu" popover>
<p>子菜单内容</p>
</div>
<!-- 打开子菜单时,父菜单不会被关闭(与 dialog 不同) -->不会阻塞页面交互
popover="auto" 模式下,弹出层不会阻止用户与页面其他部分的交互(没有焦点陷阱),这与 dialog.showModal() 不同:
html
<!-- dialog:模态显示,阻止其他交互 -->
<dialog id="modal">
<p>模态框内容</p>
<button onclick="this.closest('dialog').close()">关闭</button>
</dialog>
<!-- popover="auto":非模态,可以继续与页面交互 -->
<div id="tip" popover="auto">
<p>弹出提示内容</p>
</div>最佳实践
1. 使用 popover 替代自定义工具提示
html
<!-- 推荐:使用原生 popover -->
<button popovertarget="tooltip">悬浮查看详情</button>
<div id="tooltip" popover>
<p>这是详细信息说明。</p>
</div>
<!-- 避免:复杂的自定义实现 -->
<!-- <div class="custom-tooltip">...</div> -->2. 添加适当的 ARIA 属性
html
<!-- 虽然 popover 内置无障碍支持,但可以补充语义 -->
<button
popovertarget="menu"
aria-haspopup="true"
aria-expanded="false">
打开菜单
</button>
<div id="menu" popover role="menu">
<button role="menuitem" popovertarget="menu" popovertargetaction="hide">选项 1</button>
<button role="menuitem" popovertarget="menu" popovertargetaction="hide">选项 2</button>
</div>
<script>
// 同步 aria-expanded 状态
const menu = document.getElementById('menu');
menu.addEventListener('toggle', () => {
const btn = document.querySelector('[popovertarget="menu"]');
btn.setAttribute('aria-expanded', menu.matches(':popover-open'));
});
</script>下一节
继续学习:inert 非交互