height: 0 → height: auto 的 transition 无效,因浏览器无法对 auto 插值;应改用 max-height + scrollheight 动态赋值,并监听 transitionend 精确清空样式,兼顾移动端与可访问性。

为什么 height: 0 → height: auto 加 transition 一定无效
浏览器无法对 auto 做数值插值——transition 需要明确的起始和结束像素值才能逐帧计算。写成 height: auto 就等于告诉浏览器“你看着办”,它真没法办。常见现象是菜单瞬间闪出、或完全不动、或只闪一下就消失。
真正能动效的路径只有一条:固定像素值 → 固定像素值。比如用 max-height 替代 height,因为 max-height 支持过渡且可设为具体数值。
- 初始状态必须是
max-height: 0+overflow: hidden+opacity: 0+visibility: hidden - 展开时不能直接设
max-height: auto,得靠 JS 读取element.scrollHeight动态赋值 - 收起时不能立刻清空内联样式,否则动画跳变;必须先设
menu.style.maxHeight = '0px',再监听transitionend事件清空
如何用 scrollHeight 动态设置 max-height
硬写死 max-height: 500px 看似省事,但内容高度不固定时:设小了被裁剪,设大了收起拖尾(比如从 500px → 0 耗时固定,哪怕内容只有 60px)。
真实高度应由 scrollHeight 动态读取,不是靠猜。注意几个关键点:
- 展开前确保元素已渲染且可见(
display: block、visibility: visible),否则scrollHeight返回 0 - 移动端键盘弹出后布局未稳,建议延迟 100ms 再读
scrollHeight - 收起逻辑里监听
transitionend时,要校验event.propertyName === 'max-height',避免其他属性干扰 - 别用
setTimeout清空样式——用户快速连点中断动画后,残留的内联max-height会让下一次展开失败
transitionend 监听比 setTimeout 更可靠
这是最容易被忽略却最关键的一环。很多实现用 setTimeout 在 300ms 后清空 max-height,但动画可能被用户操作提前中断,或者在低性能设备上延迟更长,导致状态错乱。
transitionend 是浏览器原生事件,只在动画真正结束时触发,不受 JS 执行时机影响。正确写法是:
- 收起前先执行
menu.style.maxHeight = '0px' - 绑定一次性的监听器:
menu.addEventListener('transitionend', handler, { once: true }) - 在
handler中检查event.propertyName,确认是max-height变化才执行menu.style.maxHeight = ''
移动端和键盘场景下的断裂点
iOS Safari 或部分安卓 WebView 中,点击空白处收不起菜单,往往不是 JS 错,而是焦点流转断了链子:
-
:focus-within在这些环境里支持不稳定,纯 CSS 的:hover或:focus控制展开/收起在触屏下基本不可靠 - 键盘弹出可能把菜单顶出视口,导致
scrollHeight读错;此时需等布局稳定再读 - 收起逻辑要覆盖「焦点彻底离开整个下拉区域」,包括触发按钮、菜单本身、所有子项
- 必须加
@media (prefers-reduced-motion: reduce)查询,否则系统禁用动画时仍强制执行,违反可访问性规范
真正难的不是写出动画,而是让 max-height 始终贴合内容、让 transitionend 不漏触发、让焦点流转在各种设备上不掉链子——这三件事串起来,才叫可用的下拉动画。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











