max-height过渡需动态设值:展开前设max-height: 0 + overflow: hidden,再用JS读取scrollHeight赋值;收起时先设具体值再监听transitionend清空;配合prefers-reduced-motion和无障碍优化。

为什么max-height过渡常卡顿或失效
直接对height: auto做transition无效,浏览器无法计算auto的终点值;而用max-height是常见取巧方式,但设得太大(比如max-height: 1000px)会导致动画时间固定、收起时明显拖尾,设得太小又会截断内容。
- 真实内容高度不确定时,别硬猜
max-height值——优先用scrollHeight动态设置 - 初始状态必须是
max-height: 0+overflow: hidden,否则过渡不触发 - 从
0 → 具体值能过渡,但从0 → none或0 → auto不行,后者会跳变 - 动画时间建议控制在
0.25s–0.35s,超过0.4s人眼明显感知延迟
max-height过渡配合JS动态测量的最小可行写法
纯CSS方案不可靠,需JS读取scrollHeight并写入内联样式。关键是「展开前设max-height,收起后清空」,避免影响后续复用。
- 展开时:
element.style.maxHeight = element.scrollHeight + 'px' - 收起时:先设
maxHeight为具体值,再用setTimeout清空(触发重排),否则立即清空会跳变 - 务必监听
transitionend而非setTimeout来清理样式,防止动画中断导致残留 - 示例关键片段:
menu.addEventListener('transitionend', () => { if (menu.style.maxHeight === '0px') menu.style.maxHeight = ''; });
移动端点击穿透和焦点管理问题
下拉菜单在iOS Safari或部分安卓WebView中,展开后点空白处收起,常因事件冒泡或焦点丢失失败;更麻烦的是,键盘弹出后菜单被顶出视口,scrollHeight误判。
- 收起逻辑别只依赖
click outside,加一层blur监听document,兼容触屏失焦 - 菜单容器加
tabindex="-1",展开后立即.focus(),确保键盘Esc可关闭 - 避免对
body加overflow: hidden来防滚动——iOS会强制重绘,引发闪屏或输入框失焦 - 真机调试时重点看
getBoundingClientRect()是否返回{ height: 0 },这是display: none未清除的典型信号
无障碍与prefers-reduced-motion适配
动画不是装饰,而是交互反馈。但系统开启“减少运动”后,硬编码的transition仍会执行,可能干扰屏幕阅读器节奏或诱发眩晕。
立即学习“前端免费学习笔记(深入)”;
- 用
@media (prefers-reduced-motion: reduce)包裹过渡规则,直接设transition: none -
aria-expanded必须随状态实时更新,否则NVDA/JAWS无法告知用户菜单开闭 - 不要用
visibility: hidden替代max-height: 0——前者仍占布局空间,且aria-hidden="true"易漏设 - 动画曲线用
ease-in-out比ease更柔和,但别用cubic-bezier(0.1, 0.7, 0.1, 1)这类夸张缓动,实际体验反而生硬
真正难的不是让菜单动起来,而是动得恰到好处:快到不打断操作流,慢到能被感知,静到尊重用户偏好,稳到不因一行display: none就崩掉整个过渡链。


















