原生 是最快最可靠的语义化折叠方案,无需 js、键盘可访问、屏幕阅读器友好;但必须确保 为首个子元素,safari 对 summary::marker 支持差,需用 ::before + transform 控制图标,且不支持高度过渡动画。

用 <details></details> 和 <summary></summary> 实现语义化折叠
原生标签是最快、最可靠的选择,尤其适合 FAQ、帮助文档或表单辅助说明。它不依赖 JS,键盘可访问(空格/回车触发),屏幕阅读器能正确识别状态。
关键约束必须记住:<summary></summary> 必须是 <details></details> 的第一个子元素;否则点击无效。默认收起,加 open 属性即可默认展开:
<details open><summary>常见问题</summary><p>这里是答案内容。</p> </details>
- Safari 对
summary::marker支持不稳定,别只靠它改箭头;推荐用summary::before+transform: rotate()控制图标旋转 -
details[open] > summary::before可设为transform: rotate(90deg),配合transition实现平滑转动 - 不要给
<details></details>设height或transition—— 它内部高度是动态计算的,强行过渡会失效或抖动
需要动画?必须用 max-height + JS 切换类名
原生 <details></details> 不支持高度过渡动画。要实现“滑动展开”效果,就得放弃它的内置逻辑,改用 max-height + overflow: hidden 模拟。
核心难点不是 JS,而是 CSS 动画无法作用于 height: auto。所以得让 JS 测量内容真实高度:
- 点击时获取目标元素的
scrollHeight,设为max-height - 收起时设
max-height: 0,并确保overflow: hidden同时生效,否则可能残留滚动条 - 收起后建议清空
max-height行内样式(设为''),避免后续高度判断错乱 - 若内容高度差异大(比如从 20px 到 500px),固定写死
max-height: 500px会导致小内容“弹跳”,务必用 JS 动态读取
纯 CSS 折叠:用 input[type="checkbox"] + :checked 伪类
适合静态页面、JS 被禁用环境,或不想引入脚本的轻量场景。原理是利用复选框状态驱动相邻兄弟元素显隐。
结构顺序不能错:隐藏的 <input> → 关联的 <label></label> → 折叠内容 <div>。CSS 靠 <code>+ 选择器连接:
input[type="checkbox"] + label + .panel {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease-out;
}
input[type="checkbox"]:checked + label + .panel {
max-height: 300px; /* 必须大于内容实际高度 */
}
- 这个方案无法自适应高度,
max-height值必须预估足够大,否则内容被截断 -
<label></label>必须有for属性指向<input>的id,否则点击无效 - 多个折叠项需为每个
<input>分配唯一id,不能共用 - 不支持键盘空格切换(
<input>本身可聚焦,但 label 默认不继承可聚焦性)
多级嵌套或树形结构?别硬套 <details></details>
<details></details> 不支持可靠嵌套 —— Chrome 和 Firefox 表现尚可,但 Safari 在深层嵌套时经常失效,且无法控制父子联动(比如父展开时自动收起其他兄弟)。
真要实现手风琴或多级菜单,推荐基于 data- 属性 + JS 状态管理:
- 用
data-parent和data-level标记层级关系 - 点击标题时,先收起所有同级已展开项(除非允许多开),再切换当前项
- 展开/收起状态建议用
aria-expanded同步更新,保障可访问性 - 表格内的三级行折叠属于特殊场景,需结合
data-level过滤 +nextUntil()或循环查找相邻行,不能只靠 CSS
真正容易被忽略的,是动画结束后的 DOM 状态清理和可访问性属性同步 —— 很多人只顾着让内容“动起来”,却忘了告诉屏幕阅读器“现在展开了”或者“这个按钮已失效”。











