CSS transition 无法实现真正弹性回弹,因其仅支持单段缓动,无法描述多阶过冲与衰减震荡;必须用 @keyframes 手动定义三段关键帧:快速展开→超调→衰减归位,并配合 animation-fill-mode: forwards 和合理 cubic-bezier 包裹节奏。

为什么 CSS transition 做不出真正的弹性回弹
直接用 transition 配合 cubic-bezier(0.17, 0.67, 0.83, 0.67) 看似有“弹”,但这是匀速缓动拟合,不是物理弹簧模型。真实 ActionSheet 弹出时的「先冲过头、再小幅震荡回稳」效果,CSS 原生 transition 无法描述多阶位移+衰减,必须靠 @keyframes 手动逼近。
用 @keyframes 模拟弹簧回弹的关键帧怎么写
核心是三段:快速展开 → 超调(overshoot)→ 衰减震荡。不要试图还原完整阻尼振动公式,取 3–4 帧足够欺骗人眼:
@keyframes sheet-pop {
0% { transform: translateY(100%); opacity: 0; }
60% { transform: translateY(-10px); opacity: 1; } /* 冲过头 */
85% { transform: translateY(5px); } /* 回弹一点 */
100% { transform: translateY(0); } /* 归位 */
}
注意:opacity 只在前两帧渐变,避免闪烁;transform: translateY() 的数值需根据菜单实际高度调整,比如 200px 高菜单,-10px 超调量约 5% 是安全值。
animation 的 timing 和 fill-mode 容易错在哪
常见翻车点集中在两个参数:
立即学习“前端免费学习笔记(深入)”;
-
animation-fill-mode: forwards必须加,否则动画结束瞬间菜单会跳回translateY(100%) -
animation-timing-function: linear要设在animation简写里,不能依赖@keyframes内部的 easing —— 否则各帧之间会被强制插值,破坏手动设计的超调节奏 - 触发时别用
display: none → block切换,改用visibility: hidden → visible+opacity控制,否则动画可能被中断
完整触发样式示例:
.action-sheet {
visibility: hidden;
animation: sheet-pop 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}
.action-sheet.active {
visibility: visible;
}
这里 cubic-bezier(0.34, 1.56, 0.64, 1) 是为关键帧整体加一个轻微“起速+刹停”包裹,让首尾更自然。
移动端 Safari 的 transform 渲染兼容性坑
iOS 15.4 之前,纯 transform: translateY() 在某些机型上会触发闪烁或掉帧。必须加 will-change: transform 强制 GPU 加速,且仅在激活瞬间添加(避免常驻消耗性能):
JS 触发时建议这样写:
sheetEl.classList.add('active');
sheetEl.style.willChange = 'transform';
// 动画结束后立刻清理
sheetEl.addEventListener('animationend', () => {
sheetEl.style.willChange = 'auto';
}, { once: true });
另外,如果 ActionSheet 里有 input 或 textarea,iOS 键盘弹出会强制重排布局,导致动画错乱 —— 此时只能监听 focus 临时暂停动画类,或改用 position: fixed + top 替代 transform(牺牲部分性能换稳定性)。
物理感不在于帧数多,而在于超调量和衰减节奏是否符合直觉。调参时盯着真机反复点十次,比看代码更重要。


















