手风琴组件在网页中几乎无处不在:FAQ页面、设置面板、移动端导航菜单。开发这类组件时,通常需要写一段JavaScript来监听点击事件,关闭其他展开项,只保留当前点击的那一项。代码量不大,但每个项目都要重复实现,而且还得处理动画和可访问性细节。
HTML的<details>元素从诞生之初就能创建折叠面板,但长久以来缺少“互斥展开”的能力。2024年,一种全新的标准属性name被引入<details>中,解决了这个痛点。只需给同一组的<details>设置相同的name值,浏览器就会自动在打开一个时关闭其他所有同名的兄弟,完全不需要JavaScript介入。本文通过一个完整的FAQ页面案例,把这个特性的用法、边界和注意事项梳理清楚。
一、details/summary基础回顾
<details>是一个交互式元素,它包裹着可以折叠的内容。内部的<summary>标签定义可见的标题,点击标题会展开或收起隐藏内容。
<details>
<summary>什么是HTML?</summary>
<p>HTML是超文本标记语言,用于构建网页结构。</p>
</details>
用户点击“什么是HTML?”这一行,隐藏的段落就会显示出来。再次点击则收起。这个交互完全由浏览器处理,不依赖任何脚本。
在name属性出现之前,多个<details>之间互不影响——你可以同时打开任意个。要实现手风琴效果(同时只允许一个展开),必须借助JavaScript监听toggle事件,当有新的展开时手动关闭其他项。
二、用name属性实现纯HTML手风琴
现在,给一组<details>添加相同的name属性,它们就自动进入“手风琴模式”。当用户点击一个<summary>展开内容时,浏览器会自动关闭同组内其他所有的<details>,确保同一时间只有一个处于打开状态。
2.1 手风琴FAQ结构
<div class="faq-container">
<h2>常见问题</h2>
<details name="faq">
<summary>如何创建账号?</summary>
<p>点击右上角的“注册”按钮,填写邮箱和密码即可完成注册。</p>
</details>
<details name="faq">
<summary>支持哪些支付方式?</summary>
<p>我们支持微信支付、支付宝和银联卡支付。</p>
</details>
<details name="faq">
<summary>如何重置密码?</summary>
<p>在登录页面点击“忘记密码”,系统会向您的注册邮箱发送重置链接。</p>
</details>
<details name="faq">
<summary>可以在多台设备上登录吗?</summary>
<p>可以,您的账号可以在手机、平板和电脑上同时登录使用。</p>
</details>
</div>
这里四个<details>的name属性都设置为"faq"。页面加载后,用户可以点击任意一个问题的标题展开阅读,而一旦点击另一个问题,之前展开的那个会自动关闭。整个过程没有任何JavaScript,完全由浏览器原生行为驱动。
三、控制默认展开状态
如果希望在页面首次加载时默认展开某个特定的<details>,只需给它加上open布尔属性:
<details name="faq" open>
<summary>如何重置密码?</summary>
<p>操作步骤...</p>
</details>
即便有默认打开的项,手风琴行为仍然有效——用户点击其他项时,这个默认打开的项会自动关闭。
也可以在JavaScript中动态控制:document.querySelector('details[name="faq"]').open = true;。浏览器同样会自动关闭组内其他项。
四、允许用户完全折叠
手风琴组默认允许用户点击已展开项的<summary>将其折叠,从而让整个组都处于关闭状态。这与一些设计需求(必须始终有一个保持展开)可能不符。但浏览器对此没有提供额外的强制性属性。如果业务要求“必须至少保持一个打开”,则需要在toggle事件中检测,若所有项都关闭则重新打开最后一个关闭的项。
然而,原生手风琴模式提供了最大的灵活性:默认用户就可以收起当前项,还原一个干净的初始状态。这个行为符合多数场景的使用习惯。
五、多个独立组共存
同一个页面可以存在多个互不相干的手风琴组,只需给不同的组分配不同的name值。例如,FAQ用name="faq",设置面板用name="settings":
<!-- FAQ组 -->
<details name="faq">
<summary>问题一</summary>
<p>答案一</p>
</details>
<details name="faq">
<summary>问题二</summary>
<p>答案二</p>
</details>
<!-- 设置组 -->
<details name="settings">
<summary>个人信息</summary>
<p>修改个人信息...</p>
</details>
<details name="settings">
<summary>通知设置</summary>
<p>管理通知偏好...</p>
</details>
FAQ组和设置组各自独立运行,互不干扰。相比JavaScript实现的手风琴,这种分组非常清晰,不容易出现不同组之间误关闭的bug。
六、与toggle事件的配合
虽然手风琴行为是原生的,但在实际项目中可能还需要在展开或折叠时执行一些附加操作,比如发送分析统计、加载延迟内容等。可以监听<details>的toggle事件:
document.querySelectorAll('details[name="faq"]').forEach(detail => {
detail.addEventListener('toggle', () => {
if (detail.open) {
console.log('已展开:', detail.querySelector('summary').textContent);
// 发送统计事件或加载内容
} else {
console.log('已折叠');
}
});
});
这个事件在用户点击切换时触发,也会在手风琴自动关闭其他项时触发。可以精确追踪用户查看了哪个问题。
七、浏览器兼容性与降级处理
<details>元素本身在Chrome 12+、Edge 12+、Safari 6+、Firefox 49+中就得到了支持。而name属性的手风琴行为更新的浏览器才支持:Chrome 120+、Edge 120+、Safari 17.2+、Firefox 123+。在2025年初,这一特性的覆盖率已经超过90%。
对于尚不支持的旧浏览器,name属性会被忽略,所有<details>可以同时展开,回落为普通的折叠面板。这并非功能损坏,因为基本折叠功能仍然可用。你可以使用@supports来为支持的浏览器增加样式优化,或者用一段极简的JavaScript polyfill来模拟手风琴行为:
// 简单的polyfill,为不支持name手风琴的浏览器提供相同效果
if (!('name' in document.createElement('details'))) {
document.addEventListener('toggle', (event) => {
if (event.target.open && event.target.hasAttribute('name')) {
const name = event.target.getAttribute('name');
document.querySelectorAll(`details[name="${name}"]`).forEach(detail => {
if (detail !== event.target) detail.open = false;
});
}
}, true);
}
这个脚本只在旧浏览器上执行,不会影响原生支持的表现。
八、总结
HTML <details>的name属性让手风琴组件回归到了它本该属于的地方——HTML本身。不需要JavaScript库,不需要事件监听,不需要管理展开状态,仅仅一个属性就完成了互斥展开的全部逻辑。相比自定义实现的折叠面板,这种方案更简洁、更健壮,且天生具备可访问性。
现在起,凡是遇到手风琴样式的需求——FAQ、设置项、多步骤向导——都可以毫不犹豫地采用<details name="...">的方式。它已经足够成熟,是时候成为手风琴组件的默认方案了。

