CSS 实现加入购物车抛物线动画
问题背景
「加入购物车」的抛物线动画是电商前端的高频需求:用户点击加购按钮,一个小球沿抛物线轨迹飞入购物车图标。这个效果的核心难点在于用 CSS 画出一条抛物线。
本文将介绍三种 CSS 实现方式,不涉及复杂的 DOM 结构。
方案一:两层嵌套(最经典)
核心思想:外层负责水平匀速,内层负责垂直抛物。
<div class="box">
<div class="ball"></div>
</div>
/* 外层:水平匀速 */
.box {
animation: moveX 1s linear forwards;
}
/* 内层:垂直抛物线(先向上冲,再落下来) */
.ball {
width: 20px;
height: 20px;
background: #ff6b35;
border-radius: 50%;
animation: moveY 1s cubic-bezier(0.5, -0.5, 0.5, 1) forwards;
}
@keyframes moveX {
to { transform: translateX(300px); }
}
@keyframes moveY {
to { transform: translateY(200px); }
}
原理图解:
外层 translateX(linear 匀速)
─────────────────────────────▶
内层 translateY
⬆ 前半段往上冲(cubic-bezier y 跑到负值)
⬇ 后半段落回来(cubic-bezier y 回到正值)
关键参数 cubic-bezier(0.5, -0.5, 0.5, 1):
- 前两个参数 (0.5, -0.5) 是第一个控制点,y 为负值 → 动画前半段冲到目标上方
- 后两个参数 (0.5, 1) 是第二个控制点,y 拉回 1 → 动画后半段落回目标位置
- 这是一个三次贝塞尔曲线,不是真正的二次抛物线,但视觉上足够逼真
优点:代码最少,无需 JS 计算轨迹点
缺点:轨迹由贝塞尔曲线近似,不是精确抛物线;起点终点固定时好用,动态距离需要结合 JS 调整参数
方案二:单层 @keyframes 手写关键帧
如果不想嵌套两层,也可以在一个 div 上手动定义轨迹关键帧:
.ball {
width: 20px;
height: 20px;
background: #ff6b35;
border-radius: 50%;
animation: parabola 1s ease-in-out forwards;
}
@keyframes parabola {
0% { transform: translate(0, 0); }
20% { transform: translate(60px, -40px); }
40% { transform: translate(120px, -70px); }
60% { transform: translate(180px, -70px); }
80% { transform: translate(240px, -40px); }
100% { transform: translate(300px, 0); }
}
优点:一层 DOM,结构简单
缺点:关键帧写死,终点变化时需要重写所有百分比;轨迹不够平滑(6 个关键帧折线近似)
方案三:CSS Motion Path(现代方案)
利用 offset-path 直接画一条 SVG 路径,让元素沿路径运动:
.ball {
width: 20px;
height: 20px;
background: #ff6b35;
border-radius: 50%;
offset-path: path("M 0,0 Q 150,-100 300,200");
animation: fly 1s ease-in-out forwards;
}
@keyframes fly {
to { offset-distance: 100%; }
}
path 解析:M 0,0 Q 150,-100 300,200 是一条二次贝塞尔曲线(Quadratic Bézier),起点 (0,0),控制点 (150,-100),终点 (300,200)。
优点:轨迹精确可控,path 可以画任意曲线
缺点:兼容性一般(Chrome 116+ / Safari 16+),移动端需注意;path 写死,动态终点需要 JS 配合 style 属性修改
三种方案对比
| 方案 | DOM 层级 | 轨迹精度 | 兼容性 | 动态终点 |
|---|---|---|---|---|
| 两层嵌套 | 2 层 | 贝塞尔近似 | 优秀 | 需 JS 配合参数 |
| 手写关键帧 | 1 层 | 折线近似 | 优秀 | 需重写关键帧 |
| Motion Path | 1 层 | 精确曲线 | Chrome 116+ | 需 JS 改 path |
在线示例
实际工程中的推荐做法
真实业务中,加购按钮和购物车图标的位置是不固定的(响应式布局、不同商品列表位置不同),所以推荐方案一 + JS 动态计算:
// 1. 获取起点(按钮中心)和终点(购物车图标中心)
const btnRect = addBtn.getBoundingClientRect();
const cartRect = cartIcon.getBoundingClientRect();
const dx = cartRect.left - btnRect.left;
const dy = cartRect.top - btnRect.top;
// 2. 动态创建小球,inline style 设置动画
const ball = document.createElement('div');
ball.className = 'fly-ball';
ball.style.setProperty('--dx', dx + 'px');
ball.style.setProperty('--dy', dy + 'px');
container.appendChild(ball);
// 3. 动画结束后移除
ball.addEventListener('animationend', () => ball.remove());
.fly-ball {
position: absolute;
width: 16px;
height: 16px;
border-radius: 50%;
background: #ff6b35;
animation: fly 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
}
@keyframes fly {
to {
transform: translate(var(--dx), var(--dy));
}
}
这里用 CSS 自定义属性 --dx --dy 传递动态距离,配合 cubic-bezier 的垂直抛物效果,就能适配任意起点和终点。
总结
- CSS 中的这类“抛物线”本质上是视觉近似
- 最经典的做法是两层嵌套:外层
linear+ 内层cubic-bezier,利用贝塞尔曲线 y 值超出 0-1 的特性实现“先上后下” - Motion Path 适合轨迹需要更明确可控的场景,但要注意兼容性
- 工程中推荐方案一 + CSS 变量 + JS 动态计算距离,能适配任意位置