details + summary 可实现无 JS 可折叠侧边导航,语义化、屏幕阅读器友好、键盘支持空格/回车展开;但不支持多级嵌套和默认全展开,需注意 summary 必为 details 首子元素、链接须置于其外,可用 :has() 实现父级状态联动,动画需尊重 prefers-reduced-motion。

用 details + summary 最快实现可折叠侧边导航
不用 JS 就能做,原生语义化,屏幕阅读器友好,而且默认支持键盘展开(空格/回车)。但要注意它不支持「多级嵌套」和「默认展开所有子项」这种需求。
常见错误是直接把整个导航栏包进 details,结果点击任意地方都触发折叠——其实只有 summary 标签内的内容才是可点击的折叠控制区。
-
details是容器,summary必须是它的第一个子元素 - 想默认展开?加
open属性: - 不能用 CSS 伪类(如
::before)覆盖summary的默认箭头,得用list-style: none+ 自定义图标 - 如果导航项里有链接,确保它们放在
summary外、details内,否则点链接也会触发折叠
用 CSS :has() 实现无 JS 的父级高亮与状态联动
当用户鼠标悬停或聚焦某个子菜单项时,想让它的父级 summary 改变背景色或加粗?以前必须靠 JS 监听,现在可以用 :has()。
注意:Safari 15.4+、Chrome 105+、Firefox 121+ 才支持,旧版浏览器会直接忽略这条规则,不会报错,但也不会生效。
- 写法示例:
details:has(.submenu-item:focus)或details:has(.submenu-item:hover) - 别写成
details:hover:has(...),这样逻辑就反了——是子项状态影响父容器,不是父容器 hover 触发子项变化 -
:has()里不能用伪元素(如::after),也不能写兄弟选择器(如+ div) - 性能上没问题,现代浏览器对简单
:has()的优化已很成熟
手写 JS 控制折叠时,避开 toggleAttribute('open') 的兼容陷阱
很多教程教用 el.toggleAttribute('open'),但它在 Safari 15.2 及更早版本中不触发 toggle 事件,导致监听失效。真正稳妥的是直接操作 open 属性值。
另一个高频问题是:点击子菜单链接后,父级自动收起——这不是 bug,是 details 的默认行为(点击内部非 summary 区域会关闭)。要阻止它,得在链接上加 event.stopPropagation()。
- 正确切换写法:
el.open = !el.open,再手动派发new Event('toggle', {bubbles: true}) - 监听折叠状态变化,统一用
addEventListener('toggle', handler),不要监听click - 如果侧边栏需要「点击其他区域收起所有」,记得给
document绑click,但排除details和其子元素(用closest('details')判断) - 移动端点击穿透问题:iOS 上快速连点可能触发两次,建议加 300ms 防抖(只对 JS 控制场景)
用 prefers-reduced-motion 降级动画,别硬塞 CSS 过渡
很多人给 details 内容加 max-height + transition 做展开动画,但这是反模式:既破坏语义,又跟系统动效偏好冲突,还可能被屏幕阅读器误读为“内容被截断”。
真要加过渡,应该只作用于视觉层,并尊重用户设置。
- 检测方式:
@media (prefers-reduced-motion: reduce) { .submenu-content { transition: none; } } - 不要用
height: 0 → auto,因为auto无法过渡;改用max-height+ 足够大的固定值(比如max-height: 500px) - 展开动画建议用
opacity+transform: scaleY(),比max-height更平滑且不影响布局流 - 动画时间别超过 0.3s,否则在低配设备上反而显得卡顿
实际最难的不是实现折叠,而是让键盘用户能用 Tab 进入子菜单、用 Esc 退出、用方向键上下切换——这些得靠 role="menu" + aria-expanded + 手动焦点管理,details 原生不提供。如果项目对无障碍要求高,得放弃纯 HTML 方案,改用 ARIA Menu Pattern。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











