Web 平台 API / Dialog / Popover · 浮层与「关闭」的现代解法 / Popover API:轻量浮层的三种模式 待审核 3 / 9
popover · auto/manual/hint

Popover API:轻量浮层的三种模式

不是所有浮层都需要 <dialog> 那么「重」(模态、锁背景)。菜单、下拉、toast、tooltip 更需要一个轻量、非模态、但同样能进 top layer(无需参与 z-index 竞争)的浮层。这就是 Popover API:给任意元素加一个 popover 属性即可。属性值有三种,行为各不相同。

图 1-1 · <dialog> 与 popover 的定位对照。后者轻量、非模态,但同样能进 top layer。

1 · auto 的互斥与 light dismiss

两个 popover="auto" 菜单。点按钮打开——注意打开 B 会自动关掉 A(互斥),点浮层外部任意处会关(light dismiss),按 Esc 也会关(走 close request,与 light dismiss 是两条路径)。全程零 JS:按钮用 popovertarget 指向浮层 id 即可。

图 1-2 · 两个 `popover=
图 1-3 · 上一图所用的标记,全程零 JS,按钮用 popovertarget 指向浮层 id。

2 · manual 的常驻与叠加

popover="manual" 没有 light dismiss,也不互斥——必须显式关闭。这正是 toast 需要的行为:弹出后保持显示,且可以多条同时叠加。下面用 popovertargetaction 指定按钮是 show 还是 hide

图 2-1 · `popover=

3 · hint 的层叠位置

popover="hint" 是给 tooltip / 悬停预览准备的第三种值。它和 auto 一样可被关闭:点外部走 light dismiss、按 Esc 走 close request,但层叠规则被单独拆开,使它能浮在一个已打开的 auto 菜单之上而不把它关掉。Chrome 把 hint 与 auto 的交互简化成一套更可预测的模型,要点有三:

  • 打开 hint 不关掉无关的 auto——auto 菜单开着时照样能弹 hint,菜单不消失(旧模型在某些角落会误关)。
  • hint 只被「新的无关 auto」顶掉——或随它的祖先 auto 一起关闭;此外不受其他 popover 影响。
  • 把 auto 嵌进 hint 不再抛异常——嵌套的 auto 会优雅降级、按 hint 处理。正因如此,可定制的 <select>(其展开列表本身是个 popover)能安全地放进 popover="hint" 里。
图 3-1 · `popover=
图 3-2 · 上一图所用的标记。

4 · toggle 事件:状态变化的钩子

popover 开/关会派发 toggle(以及切换beforetoggle)。事件对象是 ToggleEvent,带 oldState / newState(值为 "open" / "closed")。常用来「打开时才懒加载内容」。

图 4-1 · togglebeforetoggle 两个事件的派发时机与 oldStatenewState 两个字段。

ToggleEvent 还有一个 source 属性,指向触发这次开关的元素——带 popovertarget 的按钮,或带 command / commandfor 的命令按钮。用它能判断「是哪颗按钮打开了我」(例如多个入口共用一个浮层时分流逻辑)。若是 light dismiss(点外部 / Esc)或用 JS showPopover() / togglePopover() 程序化触发且不传 source 选项时,sourcenull;传了 showPopover({ source }) 则会建立 invoker 关系,实测 ToggleEvent.source 即为该元素。

图 4-2 · ToggleEvent.source 的取值来源。

别和 CommandEvent.source 搞混。两者同名 source,但挂在不同事件上:ToggleEvent.source(本页,toggle 事件)给的是触发开关的元素;CommandEvent.source(见 invokers 页,command 事件)给的是发出命令的按钮。ToggleEvent.source 较新——Baseline 2026,晚于 newState;旧浏览器读到 undefined,需做降级。

图 4-3 · 两类事件的实时日志,可对照 ToggleEvent.sourceCommandEvent.source 挂在不同事件上。

别在 beforetoggle 里改动 popover 栈。beforetoggle 在开关生效前同步派发,本是用来「定位 / 懒加载」的时机(本页定位菜单正是用它)。若在其中再去 showPopover() / hidePopover() 别的 popover,会对刚要变化的栈造成重入式 mutation。Chrome 收紧了这里的检测,对这类调用更一致地抛 InvalidStateError,以杜绝循环重入。需要「联动开关」就放到 toggle(生效)里做。

图 4-4 · 在 beforetoggle 里改动 popover 栈的后果。该事件在开关生效前同步派发,重入会打乱栈序。

popover ≠ dialog。popover 默认非模态:不锁背景、背景仍可交互、不抢焦点。需要「模态、必须先处理完才能碰别的」就用 <dialog>.showModal()。两者可组合:<dialog popover> 同时拿到「可被 popovertarget 声明式触发」和 dialog 的语义。

延伸阅读:The Great CSS Expansion · Popover API · GitButler blog——近年 CSS / HTML 平台能力扩张的全景,其中 Popover API 一节讲清它「为什么能省掉 z-index 与 JS 浮层库」。