Dialog / Popover · 浮层与「关闭」的现代解法
弹窗、抽屉、tooltip、菜单、图片预览 —— 这些「盖在页面之上的浮层」, 过去要靠大量 z-index 较量、手写遮罩点击、监听 Esc、还要防止焦点逸出。 现在浏览器把这些做成了原生平台能力:统一的 top layer、<dialog> 的模态语义、 Popover API 的轻量浮层、声明式的 command / interestfor 触发器, 以及专管「关闭请求」的 CloseWatcher。
本系列把它们串成一条线:先掌握 <dialog> 的开/关,再认识 Popover, 然后用声明式按钮免 JS 驱动它们,接管 Esc / 安卓返回键、做一个带平滑过渡的图片预览, 最后看浏览器如何把这套浮层能力直接植入原生 <select>。每页都可动手操作。 若对 top layer 之外的普通层叠规则不熟,可先看 stacking-contexts 系列; 浮层的定位问题另见 anchor-positioning 系列。
这些 API 新旧不一:<dialog> / Popover 已是 Baseline; closedby / command 2025 年才铺开;interestfor 仍是实验特性。 每页都标了支持现状,不支持的地方做了降级处理 —— 用 Chrome 最新版体验最完整。
<dialog> 基础:show vs showModal
一个标签就是原生弹窗。看 showModal() 如何把它送进 top layer、自动让背景 inert、渲染 ::backdrop、把 Esc 接上;对比 show() 的非模态。再体验 autofocus、close(value) 写 returnValue、form method="dialog" 提交即关不发请求。
关闭的学问:closedby 与 light dismiss
「点遮罩关闭」从前要自己监听,现在一个 closedby 属性即可实现。切换 any / closerequest / none 三态,看点外部、按 Esc 分别能否关闭;再用 cancel 事件 preventDefault() 拦一个「尚未保存,确定要关?」,理解 close() vs requestClose()。
Popover API:轻量浮层的三种模式
给任意元素加一个 popover 属性,它就能进入 top layer 充当浮层。对比三种值:auto(点外部/Esc 自动关、互斥)、manual(只能手动关、可叠加)、hint(tooltip 类)。用 popovertarget 声明式触发,监听 toggle / beforetoggle(ToggleEvent 的 newState),厘清它与 <dialog> 的边界。
进出动画:让浮层平滑出现与消失
模态 <dialog> 与 popover 默认是瞬间开关。用三态模型(出场态 / :open / @starting-style 入场首帧)加 transition-behavior: allow-discrete(让 display 可过渡)、overlay 关键字(退场时留在 top layer 直到动画结束),让它们淡入滑入、再平滑退场,::backdrop 一起淡。popover 换成 :popover-open 即可;末尾给 prefers-reduced-motion 降级。
View Transitions:让 <dialog> 从按钮形变出来
animate.html 的 CSS 三态过渡之外的另一条路线。document.startViewTransition() 对开关前后各拍快照、自动补间;用共享的 view-transition-name 把触发按钮与对话框标成「同一物的两态」,对话框便从按钮的盒子形变放大而来(关闭再缩回)——这是 CSS transition 跨不了元素做不到的。配 Invoker Commands(command / commandfor)零 JS 触发、单个 command 拦截器注入过渡;含 request-close vs close、文字拉伸、::backdrop 容错三个坑与 reduce / 不支持降级。
声明式触发器:点击与「兴趣」
不写一行 JS 也能驱动浮层。command + commandfor 让按钮发出内置命令(show-modal / close / toggle-popover …)甚至自定义 --命令;interestfor 则把 hover / focus 变成触发器(配 popover=hint 做悬停卡片)。一张表理清「点击 popovertarget / 点击 command / 悬停 interestfor」各自的职责。
CloseWatcher:接管「关闭请求」
自行实现的抽屉/选择器,如何让 Esc 和安卓返回键都能关、又不至于一按返回就退出整个页面?new CloseWatcher() 把这些「关闭请求」统一收口:close 事件执行关闭、cancel 事件可拦截「脏状态」,还有 requestClose() / destroy()。动手做一个带「未保存拦截」的侧边栏。
实战:平滑的图片预览
把前面的能力组合起来做一个产品级图片预览:用 <dialog> 承载放大图(直接获得 top layer + 无障碍 + Esc),用 View Transitions 的同名 view-transition-name 让缩略图「放大」成大图、关闭再缩回,配合 hover 预加载避免白屏。一套完整的现代浮层组合方案。
可定制的 <select>:下拉也是 popover
换个角度看——浏览器把这套浮层能力植入了原生控件。一句 appearance: base-select 让 <select> 交出渲染权:<option> 里能放任意 HTML,而展开的那张列表就是一个 popover(进 top layer、用 anchor positioning 自动钉在按钮下)。配 <selectedcontent> / ::picker(select) / ::checkmark,键盘与无障碍全部由浏览器提供。
🔗 相关链接
📐 规范 / 文档
-
WHATWG HTML · The dialog element
html.spec.whatwg.org
<dialog>的规范本体:show()/showModal()/close()/requestClose()、closedby、cancel事件与 top layer 行为。 -
WHATWG HTML · The popover attribute
html.spec.whatwg.org
Popover API 规范本体:
popover三态、popovertarget、light dismiss、showPopover()/hidePopover()与command/interestfor触发器。 -
MDN · <dialog> / Popover API / Invoker Commands
developer.mozilla.org
规范级参考:Popover API、Invoker Commands、以及
::backdrop/inert/ top layer 的逐项定义与兼容表。 -
Can I use · <dialog>
caniuse.com
<dialog>元素与::backdrop的实时浏览器支持矩阵。 - Can I use · popover 属性 caniuse.com Popover API 的实时浏览器支持矩阵。