Skip to content

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 关闭

弹出层与传统方案对比

特性popoverdialog (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按钮/控件动作类型:showhidetoggle(默认)

编程式弹出层

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>

注意事项

浏览器兼容性

浏览器支持版本
Chrome114+
Edge114+
Safari17+
Firefox125+

对于不支持的浏览器,可以使用 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 非交互

参考链接