不用一行 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,重复标注反而会冲突。
小结
- 结构:
details > summary(summary 必须第一个),open 控制初始展开。
- 去默认样式:
list-style: none + ::-webkit-details-marker,箭头用 ::after 自绘。
- 状态全靠
details[open] 选择器,动画用 grid-template-rows: 0fr → 1fr 最稳。
- 手风琴用
name 属性分组,老浏览器优雅降级。
- 别把它当菜单用,也别在
summary 里塞按钮。