Popover API:轻量浮层的三种模式
不是所有浮层都需要 <dialog> 那么「重」(模态、锁背景)。菜单、下拉、toast、tooltip 更需要一个轻量、非模态、但同样能进 top layer(无需参与 z-index 竞争)的浮层。这就是 Popover API:给任意元素加一个
popover 属性即可。属性值有三种,行为各不相同。
<dialog> 与 popover 的定位对照。后者轻量、非模态,但同样能进 top layer。1 · auto 的互斥与 light dismiss
两个 popover="auto" 菜单。点按钮打开——注意打开 B 会自动关掉 A(互斥),点浮层外部任意处会关(light dismiss),按 Esc 也会关(走 close request,与 light dismiss 是两条路径)。全程零 JS:按钮用
popovertarget 指向浮层 id 即可。
popovertarget 指向浮层 id。2 · manual 的常驻与叠加
popover="manual" 没有 light dismiss,也不互斥——必须显式关闭。这正是 toast 需要的行为:弹出后保持显示,且可以多条同时叠加。下面用 popovertargetaction 指定按钮是 show 还是 hide。
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"里。
4 · toggle 事件:状态变化的钩子
popover 开/关会派发 toggle(以及切换前的 beforetoggle)。事件对象是 ToggleEvent,带 oldState / newState(值为 "open" / "closed")。常用来「打开时才懒加载内容」。
toggle 与 beforetoggle 两个事件的派发时机与 oldState、newState 两个字段。
ToggleEvent 还有一个 source 属性,指向触发这次开关的元素——带 popovertarget 的按钮,或带 command / commandfor 的命令按钮。用它能判断「是哪颗按钮打开了我」(例如多个入口共用一个浮层时分流逻辑)。若是 light dismiss(点外部 /
Esc)或用 JS showPopover() / togglePopover() 程序化触发且不传 source 选项时,source 为 null;传了 showPopover({ source }) 则会建立 invoker 关系,实测 ToggleEvent.source 即为该元素。
ToggleEvent.source 的取值来源。
别和 CommandEvent.source 搞混。两者同名 source,但挂在不同事件上:ToggleEvent.source(本页,toggle 事件)给的是触发开关的元素;CommandEvent.source(见 invokers 页,command 事件)给的是发出命令的按钮。ToggleEvent.source
较新——Baseline 2026,晚于 newState;旧浏览器读到 undefined,需做降级。
ToggleEvent.source 与 CommandEvent.source 挂在不同事件上。
别在 beforetoggle 里改动 popover 栈。beforetoggle 在开关生效前同步派发,本是用来「定位 / 懒加载」的时机(本页定位菜单正是用它)。若在其中再去 showPopover() / hidePopover()
别的 popover,会对刚要变化的栈造成重入式 mutation。Chrome 收紧了这里的检测,对这类调用更一致地抛 InvalidStateError,以杜绝循环重入。需要「联动开关」就放到 toggle(生效后)里做。
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 浮层库」。