Popover API:轻量浮层的三种模式
不是所有浮层都需要 <dialog> 那么「重」(模态、锁背景)。菜单、下拉、toast、tooltip 更需要一个轻量、非模态、但同样能进 top layer(无需参与 z-index 竞争)的浮层。这就是 Popover API:给任意元素加一个
popover 属性即可。属性值有三种,行为各不相同。
1 · auto:点外部就关、彼此互斥
两个 popover="auto" 菜单。点按钮打开——注意打开 B 会自动关掉 A(互斥),点浮层外部任意处或按 Esc 也会关(light dismiss)。全程零 JS:按钮用 popovertarget 指向浮层 id 即可。
2 · manual:不会自己关,能叠在一起
popover="manual" 没有 light dismiss,也不互斥——必须显式关闭。这正是 toast 需要的行为:弹出后保持显示,且可以多条同时叠加。下面用 popovertargetaction 指定按钮是 show 还是 hide。
3 · hint:浮在 auto 之上、不打断它
popover="hint" 是给 tooltip / 悬停预览准备的第三种值。它和 auto 一样支持 light dismiss(点外部 / Esc 关),但层叠规则被单独拆开,使它能浮在一个已打开的 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")。常用来「打开时才懒加载内容」。
ToggleEvent 还有一个 source 属性,指向触发这次开关的元素——带 popovertarget 的按钮,或带 command / commandfor 的命令按钮。用它能判断「是哪颗按钮打开了我」(例如多个入口共用一个浮层时分流逻辑)。若是 light dismiss(点外部 /
Esc)或用 JS showPopover() / togglePopover() 程序化触发,source 为 null。
别和 CommandEvent.source 搞混。两者同名 source,但挂在不同事件上:ToggleEvent.source(本页,toggle 事件)给的是触发开关的元素;CommandEvent.source(见 invokers 页,command 事件)给的是发出命令的按钮。ToggleEvent.source
较新——Baseline 2026,晚于 newState;旧浏览器读到 undefined,需做降级。
别在 beforetoggle 里改动 popover 栈。beforetoggle 在开关生效前同步派发,本是用来「定位 / 懒加载」的时机(本页定位菜单正是用它)。若在其中再去 showPopover() / hidePopover()
别的 popover,会对刚要变化的栈造成重入式 mutation。Chrome 收紧了这里的检测,对这类调用更一致地抛 InvalidStateError,以杜绝循环重入。需要「联动开关」就放到 toggle(生效后)里做。
popover ≠ dialog。popover 默认非模态:不锁背景、背景仍可交互、不抢焦点。需要「模态、必须先处理完才能碰别的」就用 <dialog>.showModal()。两者可组合:<dialog popover> 同时拿到「可被 popovertarget 声明式触发」和 dialog 的语义。
延伸阅读:The Great CSS Expansion · Popover API · GitButler blog——近年 CSS / HTML 平台能力扩张的全景,其中 Popover API 一节讲清它「为什么能省掉 z-index 与 JS 浮层库」。