以前做那种“常见问题”列表,我们总习惯给每一个<details>加一个点击事件,当打开某一个时把其他全部关闭。因为原生details默认互不影响,你开了多个,它就老老实实全开着。想做成手风琴效果,必须自己监听toggle事件,然后去关掉别的。麻烦倒是不麻烦,但每次复制粘贴这些代码总感觉不够优雅。
前几天我准备偷懒,翻了一下MDN,突然看到<details>支持一个叫name的属性,作用是把一组details变成“互相排斥”的手风琴。当场就把它用在一个项目里,成功淘汰掉16行JS。原来现在原生HTML已经把这种交互想好了,只是知道的人并不多。
先复习一下details的正常用法
一个最基础的折叠块大概长这样:
<details>
<summary>iPhone支持无线充电吗?</summary>
<p>支持。</p>
</details>
点击summary,下方的内容就展开。再点一次,它又收回去。这个元素根本不需要JavaScript,就能完成开和关。但问题是有两个这样的details,用户开了一个,另一个还保持原来的状态。
name属性带来了什么
你只需要在多个details上使用相同的name,浏览器会自动保证同一组里只能有一个处于展开状态。当打开其中一个时,同组其他所有已经展开的details都会自动收起。这就叫“手风琴模式”。
拿上面那个例子改一下:
<details name="faq"> <summary>iPhone支持无线充电吗?</summary> <p>支持。</p> </details> <details name="faq"> <summary>AirPods防水吗?</summary> <p>不防水,但防汗。</p> </details>
现在你打开“iPhone支持无线充电吗?”,然后你再点开“AirPods防水吗?”,你会发现前一个details自动被你打开了。反之亦然。
这里最关键的是:你不用在summary上放什么绑定,也不用去监听事件。浏览器原生就认识这个name属性,它把name相同的details划分成同一个互斥组。
真正的手风琴场景:更多例子
除了FAQ列表,手风琴还常用于侧边栏的菜单分组,或商品参数的筛选层。以前得用一个组件库或自己写递归折叠状态,现在一行name就完事。比如做一个有三级分类筛选的面板,每一级是一组details:
<details name="category" open>
<summary>手机数码</summary>
<p>手机壳 / 充电器 / 耳机</p>
</details>
<details name="category">
<summary>家用电器</summary>
<p>冰箱 / 洗衣机 / 空调</p>
</details>
<details name="category">
<summary>服饰鞋包</summary>
<p>T恤 / 运动鞋 </p>
</details>
这样三个分类只能展开一个,很像微信小程序里的手风琴折叠菜单,只是我们一行JS都没写。
如果我想让某些details不在同一组怎么办
name属性的原理就是“同名成组”。如果你的页面里有两组不同用途的手风琴,比如一组是FAQ,一组是产品参数,那就给它们使用不一样的name值。比如第一组用name=”faq”,另一组用name=”spec”。
<!-- FAQ组 -->
<details name="faq">
<summary>你们发货到哪些地方?</summary>
<p>全国大部分地区</p>
</details>
<details name="faq">
<summary>运费怎么算?</summary>
<p>满99免运费</p>
</details>
<!-- 参数组 -->
<details name="spec">
<summary>尺寸</summary>
<p>159.2mm × 72.7mm × 8.9mm</p>
</details>
<details name="spec">
<summary>重量</summary>
<p>约210g</p>
</details>
这样控制起来非常自由。你把name理解成“组名”就简单了。
它在浏览器里的默认交互是什么样的
有一点值得注意的是,当同组里另一个details被打开时,之前展开的那个会先关闭,然后再显示新打开的。整个切换过程不带什么动画,这跟以前自己写的slideUp、slideDown完全不一样。因为details自身的显隐是由浏览器控制的,CSS无法过渡它内部内容的平滑高度变化。
如果你想要平滑动画,目前只能借助CSS的interpolate-size或calc-size()等,或者依旧自己写JS。不过对多数手风琴场景而言,这种生硬切换并不是不能接受。
name属性的值有什么限制吗
它的名字不能包含空格。其实和表单控件的name规则类似,你把它当作普通的HTML属性值来看待,用字母开头,不加特殊符号就行了。如果你写多个值或用空格隔开,浏览器会认为这是两个不同的组名,无法形成互斥。
另外,你可以在一个details上只写一个name,但如果你主动给它赋值为空字符串,那就等于没有name。浏览器不会把两个空的name视为同一组。
从可访问性角度看它好不好用
原生的details天然支持键盘操作:你可以用Tab聚焦到summary,然后按空格或回车开合。多个details用相同name后,这个组依然是纯原生行为,不干扰屏幕阅读器的语义。反而比我们自己用aria-expanded瞎维护更靠谱。你不用担心测试时读屏软件播报状态不一致,因为浏览器内部已经同步了。
哪些浏览器支持?还得看下兼容性
这个属性是最近几年加入的。从基线来看,Chrome和Edge从120左右就支持了,Safari从17.2开始正式支持,Firefox在130版本也补上了。到了2025年,基本上现代浏览器都已经能放心使用。如果你还要兼容很老的系统,那就要做个回退。
一个简单的回退方案是用CSS @supports来检测吗?目前并没有一个专门针对details name的检测手段,不过你可以先自己判断浏览器是否有这个行为。
const supportsDetailsName = (() => {
const d = document.createElement('details');
d.setAttribute('name', 'test');
const d2 = document.createElement('details');
d2.setAttribute('name', 'test');
document.body.appendChild(d);
document.body.appendChild(d2);
d.open = true;
d2.open = true;
const result = !d.open;
d.remove();
d2.remove();
return result;
})();
如果检测到不支持,就用JavaScript手动实现互斥折叠:点击时先把同组其他details的open设为false。
if (!supportsDetailsName) {
document.querySelectorAll('details[name="faq"]').forEach((details, _, all) => {
details.addEventListener('toggle', () => {
if (!details.open) return;
all.forEach(other => {
if (other !== details) other.open = false;
});
});
});
}
但如果你不需要兼容那么老的浏览器,其实可以裸奔。我自己的目标就是内部后台,完全没负担。
一个小细节:可以配合open属性控制初始状态
你可以在其中一个details上添加一个open属性,这样页面加载后默认展开这一项。同样,同组里的另外details不能有open,否则它们都会打开,但当你第一次手动打开任意一个时,互斥规则才会生效。
<details name="faq" open>
<summary>需要注册账号吗?</summary>
<p>首次使用需要注册。</p>
</details>
<details name="faq">
<summary>可以退款吗?</summary>
<p>7天无理由退换。</p>
</details>
这样加载时默认展开第一个。另一个折叠。
实际中我用它改造了一个老旧帮助中心
上个月帮朋友维护一个老后台,里面很多帮助文档写了像下面这样一套手风琴:
<div class="acc-item active">...</div> <div class="acc-item collapse">...</div>
它的JS里甚至用了三四种类名去切换显隐,还引入了一个jQuery插件。我用details name重构了一遍,只花了一个多小时。
我把原来的标题部分变成<summary>,把内容区直接塞进details。很多原来因为JS动态效果而额外添加的wrapper span全没用了。页面HTML结构直接从一百多行瘦到几十行。最舒服的是,不用调状态同步,也不会出现“开着两个但panel显示不全”的bug。
如果有什么让我觉得遗憾,那就是:为什么这么方便的原生属性,不早一点普及?
总结:HTML真有很多被忽略的硬通货
像details本身已经存在了十几年,然而name属性却让人重新认识了它。可见HTML标准并没有停滞,只是我们脑子里老写着一套“必须自己撸JS”的惯性。偶尔看一眼MDN的更新日志,会发现不少从前要费劲封装的效果,现在原生就带支持。
如果以后再遇到手风琴菜单,先别急着引组件库,动手在details上加一个相同name属性试试。大概十秒钟就能确认你要的效果符不符合。如果符合,那今天这杯奶茶钱都省了。

