在CSS实现子元素折叠展开效果时,很多开发者会发现直接设置height属性做过渡会出现动画不流畅、跳变甚至完全不生效的情况,核心原因是浏览器无法对height从0到auto的过渡进行插值计算,导致过渡逻辑失效。使用animation配合keyframes可以绕过这个限制,实现自然的折叠展开动画。

问题分析
常见的错误实现方式是直接对height做transition过渡,代码如下:
/* 错误示例 */
.collapse {
height: 0;
overflow: hidden;
transition: height 0.3s ease;
}
.collapse.open {
height: auto;
}
这种写法中,height从0切换到auto时,浏览器无法计算中间过渡值,因此不会出现平滑的动画效果,只会瞬间切换状态,看起来非常生硬。
解决方案:使用animation和keyframes
我们可以通过keyframes定义明确的高度变化过程,再配合animation触发动画,具体步骤如下:
1. 定义keyframes动画
先定义两个动画,分别对应展开和折叠的过程,这里需要提前知道子元素的大致高度,或者通过JS动态获取高度后赋值给keyframes。
/* 展开动画,假设子元素高度为200px */
@keyframes expand {
from {
height: 0;
}
to {
height: 200px;
}
}
/* 折叠动画 */
@keyframes collapse {
from {
height: 200px;
}
to {
height: 0;
}
}
2. 绑定动画到元素
给需要折叠展开的元素设置overflow为hidden,避免内容溢出,然后通过类名切换触发不同的动画。
.target-element {
overflow: hidden;
/* 初始状态为折叠 */
height: 0;
}
.target-element.expand {
animation: expand 0.3s ease forwards;
}
.target-element.collapse {
animation: collapse 0.3s ease forwards;
}
3. 配合JS动态控制
如果子元素高度不固定,可以通过JS获取子元素的实际高度,再动态修改keyframes或者直接将高度赋值给animation的结束值,示例代码如下:
// 获取目标元素和子元素
const target = document.querySelector('.target-element');
const child = target.querySelector('.child-content');
// 切换展开折叠状态
function toggleCollapse() {
if (target.classList.contains('expand')) {
// 当前是展开状态,执行折叠
const currentHeight = child.offsetHeight;
target.style.height = currentHeight + 'px';
// 触发重排后设置折叠动画
setTimeout(() => {
target.style.height = '0';
target.classList.remove('expand');
target.classList.add('collapse');
}, 10);
} else {
// 当前是折叠状态,执行展开
const childHeight = child.offsetHeight;
target.style.height = '0';
target.classList.remove('collapse');
target.classList.add('expand');
// 设置展开的目标高度
setTimeout(() => {
target.style.height = childHeight + 'px';
}, 10);
// 动画结束后移除固定高度,避免内容变化后高度不对
target.addEventListener('transitionend', () => {
if (target.classList.contains('expand')) {
target.style.height = 'auto';
}
}, { once: true });
}
}
注意事项
- 使用keyframes时,如果子元素高度动态变化,需要每次展开前重新获取子元素高度,避免动画高度和实际内容高度不匹配。
- 动画结束后如果保留固定高度,当子元素内容变化时会出现显示异常,建议在展开动画结束后将height设置为auto,折叠动画结束后将height设置为0。
- 如果不需要兼容太老的浏览器,也可以使用CSS的
grid-template-rows属性实现0到1fr的过渡,同样可以实现平滑的折叠展开效果,不需要依赖keyframes。
替代方案:使用grid布局过渡
如果不需要支持IE浏览器,可以使用grid布局的grid-template-rows属性实现过渡,代码如下:
.target-element {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.3s ease;
overflow: hidden;
}
.target-element.open {
grid-template-rows: 1fr;
}
.target-element > .child-content {
min-height: 0;
}
这种方式不需要定义固定的高度,也不需要keyframes,浏览器会自动计算过渡过程,实现更灵活的平滑折叠展开效果。