⭐ 推荐:社区规则条款 V1.0

HTML details/summary 标签:纯 CSS 实现折叠面板

aixiu
aixiu 正式会员正式会员认证极客认证极客
发布于 2026-10-11 09:12 ·4 浏览 ·3 回复
内容摘要

介绍用 details/summary 与纯 CSS 实现可动画、互斥、键盘可用的折叠面板,核心做法包括自绘箭头、grid-template-rows 过渡、相同 name 手风琴,并建议动画优先用 grid 方案以兼顾兼容性。

不用一行 JavaScript,你就能做出带过渡动画、点开互斥、键盘可用的折叠面板——这篇把 details/summary 从最简写法讲到几个真实项目里的坑。

第一步:先写出能跑的最小结构

details 是原生折叠容器,summary 是它的标题行,点标题就展开/收起,浏览器自带键盘支持(Enter/Space 切换、Tab 聚焦)。

<details>
  <summary>运费怎么算?</summary>
  <p>满 99 元包邮,其余地区 8 元。</p>
</details>

默认是收起状态。想让它一进页面就展开,加上布尔属性 open:

<details open>…</details>

读和写状态用 JS 只有两个入口:el.open 和 el.toggleAttribute('open')。

注意:summary 必须是 details 的第一个子元素,否则浏览器不会把它当标题,而是显示一个默认的「详细信息」文字。

第二步:干掉默认三角,换成自己的箭头

默认那个小黑三角来自 list-style 和 WebKit 私有伪元素,两行清掉:

summary {
  list-style: none;
  cursor: pointer;
}
summary::-webkit-details-marker { display: none; }

然后用 ::after 自己画一个,并用 [open] 属性选择器控制旋转:

summary::after {
  content: "▸";
  display: inline-block;
  margin-left: 6px;
  transition: transform .2s;
}
details[open] summary::after { transform: rotate(90deg); }

details[open] 是纯 CSS 拿到展开状态的唯一途径,后面的动画和配色全靠它。

第三步:给展开过程加过渡

details 内容高度是 auto,直接 transition: height 是不生效的。常用的解法是把它包一层 grid,让 grid-template-rows 从 0fr 过渡到 1fr:

<details class="panel">
  <summary>什么是 GEO?</summary>
  <div class="body"><div class="inner">正文内容……</div></div>
</details>
.panel .body {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows .3s ease;
}
.panel[open] .body { grid-template-rows: 1fr; }
.panel .inner { overflow: hidden; }

内层必须有 overflow: hidden,否则文字会在收起时露出来。

注意:Chrome 129+ 起支持 interpolate-size: allow-keywords,可以直接 height: 0 → auto 过渡,写法更短,但旧浏览器不认。面向大众站点建议还是用 grid 方案,兼容面广得多。

第四步:做手风琴(同时只开一个)

给同一组 details 加相同的 name 属性即可,不用写 JS:

<details name="faq"><summary>问题一</summary>…</details>
<details name="faq"><summary>问题二</summary>…</details>
<details name="faq"><summary>问题三</summary>…</details>

同 name 的一组里,打开一个会自动关掉其他。Chrome 120+、Safari 17.2+、Firefox 130+ 已支持;老浏览器会退化成「可以同时全开」,功能不受损。

要控制默认展开哪一项,只给那一项加 open;name 组里不允许同时有多个 open,浏览器会保留第一个。

第五步:几个容易踩的细节

  • 标题语义:想让折叠标题进大纲,可以写成 <summary><h3>标题</h3></summary>,视觉上要自己清掉 h3 的 margin。
  • <summary> 里别放交互元素:按钮、链接放在 summary 里,点击会同时触发折叠,行为很乱,要放就放到内容区。
  • 不要拿它当下拉菜单:点页面其他地方不会自动关闭,也没有定位层,右键菜单、下拉选择请用别的方式。
  • 搜索会自动展开:浏览器 Ctrl+F 命中藏在 details 里的文字时会临时展开,这是原生行为,不用管。
  • 打印:多数浏览器打印时会输出全部内容,如果不想这样,加一条 @media print { details:not([open]) .body { display: none; } }。
  • 无障碍:details/summary 已经自带展开状态语义,不要额外手写 aria-expanded,重复标注反而会冲突。

小结

  1. 结构:details > summary(summary 必须第一个),open 控制初始展开。
  2. 去默认样式:list-style: none + ::-webkit-details-marker,箭头用 ::after 自绘。
  3. 状态全靠 details[open] 选择器,动画用 grid-template-rows: 0fr → 1fr 最稳。
  4. 手风琴用 name 属性分组,老浏览器优雅降级。
  5. 别把它当菜单用,也别在 summary 里塞按钮。
本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-785.html
转载请注明出处,版权归原作者所有。

全部回复 3

dp32323
dp32323 正式会员正式会员 1楼 2026-10-11 09:21

补一个能省掉手风琴 JS 的点:details 现在原生支持分组互斥,加个 name 属性就完事。

<details name="faq">…</details>
<details name="faq">…</details>

同名一组内同时只允许一个展开,点已展开的还能收起,浏览器自己管。Chrome 120+ / Safari 17.2+ / Firefox 130+ 支持,老浏览器退化成可以多开,不报错,属于纯渐进增强,不用做特性检测。

另外 grid 0fr 那套方案有个副作用值得留意:收起时内容只是被 overflow 裁掉了,节点还在渲染树里,里面的链接、按钮照样能被 Tab 聚焦,屏幕阅读器也可能念到,键盘用户会"跳到看不见的地方"。补一句就好:

.panel:not([open]) .inner { visibility: hidden; }

visibility 能参与 transition,不影响展开动画。

runyu
runyu 正式会员正式会员认证极客认证极客 #650 2楼 2026-10-11 09:23
dp32323:补一个能省掉手风琴 JS 的点:`details` 现在原生支持分组互斥,加个 `name` 属性就完事。 ```html … … ``` 同名一组内同时只…

name 分组和 visibility 这两个补丁都补得准,尤其焦点那个坑——overflow: hidden 裁掉的只是视觉,Tab 顺序和可访问性树里的节点一个没少。

关于 name 再补一个容易翻车的点:互斥范围是整个文档,不分父容器。同一个页面里如果放了两个不同区块、但都随手写了 name="faq",它们会跨区块互相挤掉。建议按区块取具体值,比如 name="faq-shipping"、name="faq-refund"。另外组内初始 HTML 里若手滑写了两个 open,浏览器加载时只会保留最后一个,渲染出来不会报错,但很容易让人以为样式丢了。

visibility 那句还有个小前提:它必须出现在 transition 的属性列表里,否则收起瞬间内容就没了,动画看起来像"容器在空转"。写在 .inner 上就行:

.panel .inner { overflow: hidden; transition: visibility .3s; }
.panel:not([open]) .inner { visibility: hidden; }

从 visible 到 hidden 的离散插值会在过渡结束那一刻才切到 hidden,展开方向则一开始就可见,方向天然是对的,不需要 allow-discrete。

再往下如果有 iframe、视频这类重内容,visibility: hidden 只是不渲染,资源该加载还是加载,可以配合 loading="lazy" 或 content-visibility: hidden 用。

zero
zero 见习用户见习用户 #651 3楼 2026-10-11 09:28
runyu:`name` 分组和 `visibility` 这两个补丁都补得准,尤其焦点那个坑——`overflow: hidden` 裁掉的只是视觉,Tab 顺序和可访问…

name 全局作用域这个提醒很值,但比"跨区块挤掉"更隐蔽的是它连带触发的 toggle 事件——组内其他面板被自动关掉时,一样会冒泡 toggle。

所以如果你用 toggle 做懒加载、埋点或发请求,一次点击可能收到 N 次回调。判断方向别用"有没有 open"这种取反的写法,直接读事件对象:

details.addEventListener('toggle', e => {
  if (e.newState === 'open') { /* 真正展开了才跑 */ }
});

顺带说下你说的初始多个 open:解析是按文档顺序走的,遇到同名已展开的就关掉前一个,最终剩最后一个,和你观察一致。值得注意的是这个自动关闭同样是 toggle 的一部分——静态 HTML 里因为监听器还没挂上所以无感,但如果你是动态 innerHTML 插入一组面板,回调就会被打到,这点在 SSR 拼字符串的场景下要留意。

visibility 必须进 transition 列表这条也补得准,方向天然对是因为 visible→hidden 的离散插值取结束值、hidden→visible 取起始值。如果想更彻底一点(不可聚焦 + 移出可访问性树 + 不可点击),inert 是最干净的,但 CSS 设不了这个属性,纯 CSS 方案里 visibility 基本就是最优解了。

content-visibility: hidden 那招记得配 contain-intrinsic-size,否则展开前后滚动高度会跳,带锚点定位的页面偏移特别明显。

坑一个:收起时若焦点还在内部元素上,visibility: hidden 会把它甩到 body,键盘用户等于"原地迷路",交互敏感的页面最好把焦点收回 summary。