classList.toggle调用后CSS transitions不同步,手风琴max-height过渡如何实现?
问题根本原因
你观察到的过渡不同步是视觉错觉,两个元素的transition实际上是同时触发的:
你给展开状态设置的max-height: 100px远大于示例中内容的实际高度(约20px):
- 元素展开时,
max-height从0向100px过渡,只要数值超过内容实际高度,内容就会完全显示,所以动画从第一帧就有肉眼可见的变化 - 元素收起时,
max-height从100px向0过渡,前80px的过渡过程中max-height仍然高于内容实际高度,元素可见高度不会发生任何变化,直到max-height降到低于20px才会开始收缩,看起来就像动画延迟启动
这就导致两个元素的可见动画不在同一个时间线,产生不同步的观感。而改用固定height时过渡全程都有可见变化,所以看起来是同步的。
基于max-height的解决方案
1. 纯CSS简易适配(内容高度固定场景)
尽量把展开态的max-height设置为刚好略大于内容的最大可能高度,减少过渡空跑的时长,同时调整缓动函数抵消视觉差:
.accordion p:last-child { overflow: hidden; margin: 0; max-height: 0; /* 收起用快入曲线,快速降到内容高度以下 */ transition: max-height 0.3s cubic-bezier(0, 1, 0, 1); } .accordion.expanded p:last-child { max-height: 30px; /* 尽量接近内容实际高度,不要设置过大的冗余值 */ transition: max-height 0.3s ease-in-out; }
2. JS精准控制(动态高度内容场景)
通过JS读取内容的实际scrollHeight,控制max-height的过渡起始值始终和内容真实高度一致,完全消除空跑阶段:
JS代码:
const acs = document.querySelectorAll('.accordion'); const contentSelector = 'p:last-child'; const transitionDuration = 300; // 和CSS过渡时长保持一致 acs.forEach((a, i) => { a.addEventListener('click', () => { acs.forEach((aa, ii) => { const content = aa.querySelector(contentSelector); if (i === ii) { // 展开逻辑:先设为实际内容高度触发过渡,完成后恢复大值适配内容变化 aa.classList.add('expanded'); content.style.maxHeight = `${content.scrollHeight}px`; setTimeout(() => { content.style.maxHeight = '100px'; }, transitionDuration) } else { // 收起逻辑:先把max-height重置为当前实际高度,下一帧再设为0触发过渡 aa.classList.remove('expanded'); content.style.maxHeight = `${content.scrollHeight}px`; requestAnimationFrame(() => { content.style.maxHeight = 0; }) } }) }) })
CSS代码:
.accordion { padding: 0.5em; margin-bottom: 1em; background: #f3f3f3; } .accordion p:last-child { overflow: hidden; margin: 0; max-height: 0; transition: 0.3s linear max-height; }
该方案适配任意高度的动态内容,展开和收起动画全程可见,完全同步。
内容的提问来源于stack exchange,提问作者Toni Michel Caubet
相关产品推荐
相关产品推荐

