以前想做“手风琴”折叠面板,第一反应就是写JavaScript或者引一个组件库。一堆class切换、事件绑定动不动就几十行。但是最近我试了一下HTML原生的<details>元素加name属性,发现浏览器已经帮你把手风琴逻辑写好了。只需要一个name,同一个name下的<details>会自动互斥:开一个另一个就关掉,完全不需要脚本。
这个特性其实已经悄悄进了Chrome 120,现在Safari也跟进了。虽然还不能在老旧浏览器上使用,但做一个内部工具或者移动端页面已经足够了。我自己拿它改造了一个FAQ页面,原来绑了一堆点击事件,现在全部删掉,代码肉眼可见地变轻了。
一个最普通的details长什么样
日常用法大家应该不陌生:
<details> <summary>什么是details?</summary> <p>details是一个HTML元素,默认隐藏里面的内容,点击summary展开。</p> </details>
这样就是一个独立可展开的折叠块。但是每个<details>都是各管各的,你可以同时打开多个。想要做成“只能打开一个”的手风琴效果,就得用JS把其他的全部关掉。现在不用了,给details加上name就行。
name属性带来的互斥效果
给多个details设置相同的name,它们就变成了同一组,同一时间只有一个能展开。我用一个简单的例子说明:
<details name="faq"> <summary>第一项:为什么今天要加班?</summary> <p>因为项目马上要上线,大家需要冲刺一下。</p> </details> <details name="faq"> <summary>第二项:加班有加班费吗?</summary> <p>有,按照公司规定发放。</p> </details> <details name="faq"> <summary>第三项:明天能不能居家办公?</summary> <p>看项目进度,大概率可以。</p> </details>
在支持的浏览器里,你打开第二项的时候,第一项会自动关闭。不需要加任何样式和JS。这个行为就像原生radio单选框一样,name相同的details是互斥的。
这个特性最适合做“常见问题”“操作步骤”“条款折叠”这种场景。用户再也不用一屏显示很多展开内容,交互也清晰。
不只是简单的手风琴:还能嵌套使用
name属性很灵活,它可以和不同的name组合成复杂的折叠结构。比如我做一个商品规格说明,一级分类用name=”group1″,二级分类用name=”group2″,这样两个级别互不影响。
来看看实际例子:
<details name="group1">
<summary>尺寸信息</summary>
<details name="group2">
<summary>标准版</summary>
<p>屏幕 6.1 英寸</p>
</details>
<details name="group2">
<summary>Pro版</summary>
<p>屏幕 6.7 英寸</p>
</details>
</details>
这样可以实现:外层是手风琴,同一时间只能展示一个尺寸信息;内层也是手风琴,同一个尺寸下只能看一个版本。互不干扰,状态非常清晰。
这样的组件要是用JS写,需要判断点击的是哪个层,还要防止事件冒泡,麻烦得很。现在两个name就把分层逻辑表达得清清楚楚。
基础用法之外,怎么优雅地加动画
原生的details展开和收起是没有过渡动画的,直接弹出来。如果想加一点渐变效果,其实也不用写多少JS,只需要在CSS里配合interpolate-size或者calc-size(),但这个写法比较复杂。我一般只给summary加一个“三角箭头”的旋转,就能提升不少观感。因为本文不能使用style标签,我就不展示CSS了,你可以在自己的CSS文件里这样写:
summary::-webkit-details-marker { display: none; }
summary::before { content: '▶'; }
details[open] summary::before { content: '▼'; }
这属于锦上添花,不是核心。即便不加任何样式,浏览器默认的展开折叠也能用,不影响功能。
浏览器兼容性及降级
这个特性目前主要支持Chromium内核浏览器、Edge、以及Safari 17.2+。Firefox虽然支持details很久了,但对name属性的支持还在开发中。所以如果你的项目要兼容Firefox,最好加一个判断,或者使用Polyfill。
最简单的降级方式是:用原生details本身也能用,只是多个details不再互斥,用户依然可以自由开合。也就是说,没有name属性并不会让页面坏掉,只是手风琴效果缺失。这反而是很好的“渐进增强”思路:基础体验用HTML原生能力,增强体验靠新特性。
如果你的产品必须支持Firefox并且需要手风琴,可以临时给不支持name的浏览器加一小段JS监听toggle事件。大概长这样:
if (!('name' in HTMLDetailsElement.prototype)) {
document.querySelectorAll('details[name]').forEach(details => {
details.addEventListener('toggle', function() {
const name = this.name;
document.querySelectorAll(`details[name="${name}"]`).forEach(other => {
if (other !== this) other.open = false;
});
});
});
}
这只是一段备用方案,等Firefox更新后就彻底删掉。有了原生name属性,90%的代码其实只需要写上面这几行,而且以后还能移除。
实际项目里我用它替换了组件库的手风琴
上个月维护一个后台系统的“设置帮助”页面,里面原本用的是某个UI框架里的Collapse组件,光依赖体积就不少。我把它换成了几组带name的<details>,然后删掉了相关组件的CSS和JS。最后发到测试环境,产品经理说“跟以前一样没毛病”。
有几个同事问我怎么没引JS?我说这就是浏览器自带的功能。他们一开始还不信,直到打开开发者工具看了元素,才反应过来。
如果你也受够了折叠面板动不动就一堆代码,试试这个新特性吧。它让HTML又重新有了“写起来很爽”的感觉。
几个值得注意的小细节
1. name值不要重复得太离谱。它是按“同一根节点下”互斥的,本质上好像和radio一样,但具体细节更复杂。实测中,如果两个details嵌套在不同的父级里,只要name相同,也可能会互相影响;所以如果想控制互斥范围,最好给不同区域用不同的name,像前面的group1、group2那样。
2. 通过JavaScript动态创建的details同样支持name互斥,只要插入到DOM后,浏览器会自动处理状态。不需要rescan。
3. 对于不支持name的浏览器,如果使用了上面的降级JS,可能要注意避免频繁触发。在toggle事件里修改open属性会触发其他details的toggle事件,所以要做好防循环。我的办法是加一个变量标记正在执行,就很简单。
4. 如果你想刚打开页面时就默认显示某一项,可以在<details>上加open属性。浏览器会把它当作默认展开项,但在手风琴组里如果某一项默认展开,其他项再操作就会正常互斥。注意默认展开项只能有一个,如果有多个的话,后出现的会先被浏览器关掉。
总结
HTML的details name属性是我最近觉得最舒服的一个原生增强。它用最简单的方式实现了原来需要几十行JS的交互。当然,它做不到那种复杂的“手风琴带禁用状态”,但很多时候我们要的只是“别让我全都展开”而已。
以后当你再看到手风琴组件,先别急着找组件库,也许一个<details name="...>就够了。

