用原生HTML details元素替代手风琴交互?我试了

阿乐
阿乐 管理员 黑卡会员
发布于 2026-09-07 02:06 ·1 浏览 ·0 回复

最近在做一个小项目,又双叒碰到「手风琴」交互需求——点开一个面板,其他面板自动收起。按老习惯,我准备祭出 jQuery 插件或者 Bootstrap 折叠组件,但转念一想:现在浏览器都原生支持 `<details>` 元素了,何必再拖一堆脚本?于是决定试试用原生 HTML 重写手风琴。试完之后,感想还挺多,跟各位分享下。

为什么我会想用 details 替换手风琴

手风琴的本质,其实就是「可折叠区域 + 互斥展开状态」。传统做法要自己维护 `aria-expanded`、管理 CSS 类名、写 JS 开关逻辑,多一步都嫌烦。而 `<details>` + `<summary>` 这两个标签,天生就是干这个的:

- 用户点击 `/summary` 自动切换 `/details` 的展开状态;
- 展开/收起状态由浏览器原生管理,默认无需 JS;
- 每个面板就是一个独立的标签结构,屏幕阅读器也能读出状态。

如果把所有手风琴面板都塞进一个容器,然后只让它们共享互斥逻辑,那理论上就能完美替换。这诱惑力太大了——不用引依赖、不用写事件绑定、代码量直接腰斩,谁能不心动?

手风琴与 details 的关键差异

不过真动手才发现,原生 `<details>` 并不等于手风琴。手风琴的招牌行为是「同时最多只能展开一项」,但 `<details>` 默认允许所有面板同时展开,互不干扰。想要还原手风琴特性,你得自己做互斥。

这个倒不难。监听 `toggle` 事件,打开某个面板时,把同一组里其他面板的 `open` 属性去掉就行。注意别在页面初始渲染时误触发(只处理用户交互的 `toggle` 即可),否则容易把默认展开的第一项也关了。

const panels = document.querySelectorAll('.accordion details');

panels.forEach(panel => {
  panel.addEventListener('toggle', e => {
    if (!e.target.open) return;
    panels.forEach(other => {
      if (other !== e.target) other.open = false;
    });
  });
});

但这样写,动画效果就很尴尬了——默认 `details` 是瞬间展开收起的,没有任何补间过渡。相比之下,传统手风琴插件普遍自带滑动动画,看着更丝滑。不过这也不是不能治,通过 CSS 动画能给 `[open]` 加淡入或轻微高度过渡,但没法像 JS 插件那样做到流畅的 `max-height` 滑动。愿意调一调,倒是可以接近,而非天生相似。

而且最让我兴奋的是,现代浏览器还提供了一个新属性:`name`。给一组 `<details>` 设置相同的 `name` 值,浏览器会强制它们互斥——一个展开,另一个自动收起。这不就是原生手风琴本人吗?

<details name="faq">
  <summary>第一题</summary>
  <p>答案在这</p>
</details>
<details name="faq">
  <summary>第二题</summary>
  <p>答案在那</p>
</details>

不过也要提醒一句:这个属性支持时间还不算特别久,老浏览器可能忽略它,退化为多个展开。如果项目不用兼容 IE 和太远古的浏览器,这个方案真是又简单又干净。

样式与体验细节

视觉上,`summary` 默认会显示一个小三角 marker,想让它消失可以设置 `summary { list-style: none; }`。间距、边框、背景色都比我想象中好搞,因为部分状态动态暴露在属性里,可以直接写:

details {
  border-bottom: 1px solid #ddd;
}

details summary {
  padding: 16px;
  cursor: pointer;
}

details[open] summary {
  font-weight: 600;
  color: #1a73e8;
}

需要动画的话,可以借用 CSS `@keyframes` 对 `[open]` 里的内容做 fade-in,虽然做不到旧式汉化插件那种优雅的平滑伸缩,但胜在轻量。

可访问性的思考

这一点我是真心满意的。原生 `<details>` + `<summary>` 的对焦点、状态提示、键盘交互都已内建,屏幕阅读器也支持良好,完全不用自己给 `div` 塞 `aria-expanded` 和 `role="tab"` 那套。写了这么久的自定义折叠面板,回头看这种原生语义,简直是救赎。

唯一要留神的,是如果把多个 `details` 组件用 `name` 互斥,本质上它还是「一组独立开关」的语义,未必等同于手风琴 tab 的「当前激活面板」语义。对绝大多数帮助中心、FAQ 场景来说完全没问题,但如果是那种强需要箭头指向激活项的交互,可能还是需要上自定义组件。

我的结论

用原生 `<details>` 替代手风琴,在我看来完全可行,而且应该成为一个默认选择——特别是那些「也许一个页面有 3-5 个问题,需要快速隐藏」的简单场景。直接写 `<details name="...">` 就能得到原生互斥展开,几乎零成本拿到一个语义化、可访问性俱佳的折叠面板。

但如果你是做那种追求高度自定义动画、联动逻辑特别复杂、还要支持老旧浏览器的企业应用,那保留成熟的手风琴组件仍是有道理的。至少我这次试下来,原生方案已经能覆盖我遇到的一大半需求,而且代码清爽很多。

以后遇到这个交互,我不打算急着引插件了,先试试 `<details>`——它真的比你想象的更能打。

全部回复 0

还没有回复,来抢沙发~