原生 HTML 交互组件实战:details、dialog、popover 搭建一个设置面板

2026-11-01 0 370

上个月接手一个后台项目,设置页面里塞了三个交互:折叠分组、删除确认弹窗、以及一堆鼠标悬停才出现的小提示。它们分别来自三个不同的 UI 库,加起来压缩后一百多 KB,还带来两套完全不同的弹层层级逻辑。两个弹层同时打开的时候,谁盖住谁全靠 z-index 猜。

后来翻了下这几个交互的规范,发现浏览器早就有对应的原生元素了。这篇文章就把这套设置面板重写一遍,只用 details、dialog 和 Popover API,顺带记录兼容性和实际遇到的问题。代码都是可以直接跑的。

一、折叠分组:details 的 name 属性

以前做手风琴,要么自己写一套 open/close 状态,要么让 UI 库接管 DOM。现在只需要给同一组 details 加一个相同的 name 值,浏览器会自动保证同时只展开一个。

<div class="settings-group">
  <details name="settings" data-group="settings" open>
    <summary>账号与安全</summary>
    <p>绑定手机号、修改登录密码、开启二次验证。</p>
  </details>

  <details name="settings" data-group="settings">
    <summary>通知偏好</summary>
    <p>站内信、邮件、短信三类通知可以分别开关。</p>
  </details>

  <details name="settings" data-group="settings">
    <summary>数据与导出</summary>
    <p>按月份导出订单明细,导出任务在后台排队执行。</p>
  </details>
</div>

几个容易忽略的点:

  • summary 必须是 details 的第一个子元素,否则浏览器不会把它当标题渲染,而是自己塞一个默认的「详细信息」。
  • open 是布尔属性,写 open="false" 依然是展开状态,要关闭就直接删掉这个属性。
  • 组内互斥只对同一个 name 值生效,跨组的 details 互不影响。

二、旧浏览器的降级处理

name 属性在 2024 年之后的主流版本里才陆续支持,某些还在维护的旧版浏览器里,三个分组会同时展开。这不是致命问题,但体验不一致,所以加一段很短的降级代码就够了。

const supportsNameGroup = 'name' in HTMLDetailsElement.prototype;

if (!supportsNameGroup) {
  document.querySelectorAll('details[data-group]').forEach((item) => {
    item.addEventListener('toggle', () => {
      if (!item.open) return;
      const group = item.dataset.group;
      document
        .querySelectorAll(`details[data-group="${group}"]`)
        .forEach((other) => {
          if (other !== item) other.open = false;
        });
    });
  });
}

用 toggle 事件而不是监听 summary 的点击,是因为 details 的展开状态也可能被键盘操作改变,监听点击会漏掉回车触发的场景。

三、删除确认:dialog 元素

模态框是自研代码里最容易出事的地方:焦点不锁、Esc 关不掉、点遮罩不关、关闭后焦点不知道飞哪去。dialog 配合 showModal(),这几件事浏览器都替你处理了。

<dialog id="confirm-delete" aria-labelledby="confirm-title">
  <form method="dialog">
    <h2 id="confirm-title">确认删除这条记录?</h2>
    <p>删除后不可恢复,关联的导出任务也会一并取消。</p>
    <button value="cancel">取消</button>
    <button value="confirm">删除</button>
  </form>
</dialog>

关键在于 form method="dialog"。表单提交时浏览器不会发请求,而是关闭对话框,并把被点击按钮的 value 写进 dialog.returnValue。省掉了自己维护按钮状态的那段代码。

const dialog = document.querySelector('#confirm-delete');
const deleteBtn = document.querySelector('#delete-record');

deleteBtn.addEventListener('click', () => {
  dialog.showModal();
});

dialog.addEventListener('close', () => {
  if (dialog.returnValue === 'confirm') {
    // 真正执行删除
    removeRecord();
  }
  // 无论确认还是取消,returnValue 都会保留,下次打开前可以手动清空
  dialog.returnValue = '';
});

这里有两个实际踩过的坑:

  1. showModal() 和 show() 不是一回事。show() 只是把元素显示出来,没有遮罩、没有焦点陷阱、也不会让页面其他内容变成 inert。做模态框必须用 showModal()。
  2. 对已经打开的对话框再调一次 showModal() 会抛 InvalidStateError。如果打开动作可能被重复触发(比如按钮在动画期间被连点),先判断 dialog.open。

如果业务要求「点 Esc 不能关」,就在 cancel 事件里阻止默认行为:

dialog.addEventListener('cancel', (event) => {
  if (isUploading()) {
    event.preventDefault();
  }
});

四、轻提示:Popover API

设置页里那些「鼠标移上去才出现的小气泡」,以前要么用 title 属性(样式完全不可控、触摸设备不显示),要么自己写一套定位和外部点击关闭。Popover API 把这些都标准化了。

<button popovertarget="tip-export">导出的数据包含哪些字段?</button>

<div id="tip-export" popover>
  <p>包含订单号、支付时间、金额、退款状态,不包含收货地址和手机号。</p>
  <button popovertarget="tip-export" popovertargetaction="hide">知道了</button>
</div>

不需要写一行 JavaScript。点击按钮打开,再点一次关闭,点击页面其他位置也会自动关闭。最后一条是 popover 默认模式(auto)才有的行为,如果显式写成 popover="manual",点外部不会关闭,得自己调 hidePopover()。

auto 模式还有一层「轻量弹层」的语义:同一时间只会显示一个 auto 类型的 popover,打开新的会自动关掉旧的。这一点和模态对话框不同,对话框是可以叠加的。

需要动态控制时,用 togglePopover():

const tip = document.querySelector('#tip-export');

document.querySelector('#help-link').addEventListener('click', (event) => {
  event.preventDefault();
  tip.togglePopover();
});

togglePopover() 可以传 true 或 false 表示强制显示/隐藏,不传则切换。注意它和 showModal() 一样,对不支持该 API 的环境调用会直接报错,所以升级前的项目要先做一次特性检测:

if (typeof HTMLElement.prototype.togglePopover === 'function') {
  // 走原生实现
} else {
  // 退回自己的浮层组件
}

五、三个组件放进同一个页面时的注意事项

单独用都没问题,凑在一起就有细节了。

1. 弹层嵌套不要跨越 dialog 边界

在 dialog 内部放一个 popover 是可以的,但反过来不行——把 dialog 放进一个 popover 里,然后调用 showModal(),会出现焦点被重复锁定的情况,表现是 Tab 键在弹窗里循环,却按顺序跳过了浏览器地址栏。需要嵌套时,用 dialog 套 dialog,或者把 popover 移到 dialog 内部。

2. dialog 打开时,背景确实滚不动了,但你要自己处理滚动条

showModal() 会让背景内容变成 inert,但不会锁住 body 的滚动。内容很长的页面里,对话框打开后滚轮依然能滚背景,视觉上很别扭。比较稳妥的做法是在 close 和打开时切换一个控制滚动的类,或者直接在 open 属性上用 CSS 选择器控制。

3. 用 showModal 之后不要再手动加遮罩层

有些老代码习惯在弹窗外面套一层半透明 div。用了 dialog 之后这层 div 反而会挡住对话框的点击区域,因为 ::backdrop 已经存在了。要改背景颜色,直接改 ::backdrop 的样式,别加节点。

4. 屏幕阅读器对 popover 的展开状态支持还不统一

按钮上的 popovertarget 在规范里有隐式的展开状态语义,但不同屏幕阅读器的读法差异比较大。如果提示内容超过两句话,或者里面有可操作的按钮,建议改回 dialog,因为对话元素有明确的 role="dialog" 和标题关联,读屏体验稳定得多。

六、完整的最小示例

把上面的部分拼起来,一个能跑的设置面板大概是这样:

<section>
  <h2>设置</h2>

  <details name="settings" data-group="settings" open>
    <summary>账号与安全</summary>
    <p>绑定手机号、修改登录密码、开启二次验证。</p>
    <button id="delete-record">删除账号数据</button>
  </details>

  <details name="settings" data-group="settings">
    <summary>通知偏好</summary>
    <p>站内信、邮件、短信三类通知可以分别开关。</p>
  </details>

  <details name="settings" data-group="settings">
    <summary>数据与导出</summary>
    <p>
      按月份导出订单明细。
      <button popovertarget="tip-export">包含哪些字段?</button>
    </p>
    <div id="tip-export" popover>
      <p>包含订单号、支付时间、金额、退款状态。</p>
    </div>
  </details>
</section>

<dialog id="confirm-delete" aria-labelledby="confirm-title">
  <form method="dialog">
    <h2 id="confirm-title">确认删除?</h2>
    <p>删除后不可恢复。</p>
    <button value="cancel">取消</button>
    <button value="confirm">删除</button>
  </form>
</dialog>

配套的脚本只有十几行,负责三件事:打开对话框、读取 returnValue、给不支持 name 分组的环境补一段互斥逻辑。没有状态管理,没有层级计算,也没有额外的 DOM 包装节点。

七、什么时候还是该用组件库

说句公道话,原生方案不是万能的。下面这几种情况,自己写反而更累:

  • 需要下拉选择、日期选择这类复杂交互,浏览器目前没有对等元素。
  • 设计稿要求 popover 带箭头、带偏移定位(比如贴着按钮上方 8px 处),原生 popover 的定位靠 CSS anchor 相关属性,兼容性还在推进中,做全量适配成本不低。
  • 需要和已有设计系统的主题变量对齐,而系统里已经沉淀了几十个弹层组件。

但如果只是折叠、确认、轻提示这三类,原生元素已经足够,而且免去了卸载时清理事件监听、清理挂载节点这些麻烦事。少装一个依赖,也少一个半夜升级版本时的心跳。

原生 HTML 交互组件实战:details、dialog、popover 搭建一个设置面板
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请发送邮件1506151422@qq.com联系,将会及时下架删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 html 原生 HTML 交互组件实战:details、dialog、popover 搭建一个设置面板 https://www.taomawang.com/web/html/2990.html

下一篇:

已经没有下一篇了!

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务