微交互与动画设计
时间曲线、手势反馈与减弱动效模式,构建愉悦且无障碍的 UI。
组件模式高级
为什么
微交互(按钮涟漪、切换弹簧、加载微光)传达状态变化比文本快 10 倍。但调校差的动画引起前庭不适且卡顿。严谨的运动系统平衡愉悦、性能与无障碍。
何时使用
为状态转换(加载、成功、错误)、手势反馈(滑动、拖拽)和焦点引导添加动效。避免动画装饰元素与内容争夺注意力。
如何做
1. 定义运动令牌 — 持续时间(150/250/400ms)、缓动(ease-out、spring)、错峰
2. 选择库 — React 用 Framer Motion,原生用 Motion One,复杂时间线用 GSAP
3. 用弹簧而非线性 — 自然感,如 transition: { type: "spring", stiffness: 300, damping: 30 }
4. 仅动画 transform + opacity — 避免动画布局属性(width、top)以保 60fps
5. 尊重减弱动效 — 用 useReducedMotion() hook 包裹,回退到瞬时状态变化
6. 编排 — 子元素错峰 delay: i * 0.05,退出动画用 AnimatePresence
7. 性能分析 — React DevTools Profiler + Chrome Performance 标签,目标每帧 <16ms
示例
// motion-tokens.ts — 共享时间词汇
export const motion = {
fast: { duration: 0.15, ease: [0.4, 0, 1, 1] as const },
normal: { duration: 0.25, ease: [0.4, 0, 0.2, 1] as const },
spring: { type: "spring", stiffness: 300, damping: 30, mass: 1 },
stagger: { staggerChildren: 0.05 },
} as const;
// Toast.tsx — 弹簧入场 + 减弱动效守卫
import { motion, useReducedMotion } from "framer-motion";
function Toast({ message }: { message: string }) {
const reduce = useReducedMotion();
return (
<motion.div
initial={reduce ? { opacity: 0 } : { opacity: 0, y: 20, scale: 0.95 }}
animate={reduce ? { opacity: 1 } : { opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={reduce ? motion.fast : motion.spring}
className="rounded-lg bg-slate-900 px-4 py-2 text-white"
>
{message}
</motion.div>
);
}
// 列表错峰 — 子元素级联进入
<motion.ul variants={{ hidden: {}, show: motion.stagger }}
initial="hidden" animate="show">
{items.map((item) => (
<motion.li variants={{
hidden: { opacity: 0, y: 10 },
show: { opacity: 1, y: 0, transition: motion.normal }
}}>{item}</motion.li>
))}
</motion.ul>
常见问题
- ⚠动画 width/height/top — 每帧触发布局重算,掉到 20fps
- ⚠无减弱动效回退 — 前庭用户因视差和弹簧恶心
- ⚠满地线性缓动 — 机械感,无加减速曲线
- ⚠每个列表项挂载即动画 — 100 项错峰 = 5s 才出现最后一项
最佳实践
- ✓仅动画 transform 和 opacity — GPU 合成,无布局抖动
- ✓错峰上限 5-8 项 — 超过则淡入容器而非级联
- ✓始终提供 useReducedMotion 分支 — 瞬时状态变化,无动效
- ✓运动令牌定义一次 — 每个动画引用 motion.normal,非魔法数字
常见问题解答
为什么动画 transform/opacity 而非 width/top?
transform 和 opacity 由 GPU 合成,不触发布局重算。动画 width、top、left 每帧重算布局,掉到 20fps。
如何尊重减弱动效偏好用户?
用 useReducedMotion() hook(Framer Motion)或 matchMedia('(prefers-reduced-motion: reduce)')。设置时跳过动画,瞬时应用终态。
线性 vs 弹簧缓动哪个更好?
弹簧(刚度/阻尼)有加减速自然感并稳定;线性机械。交互元素用弹簧,入场用 ease-out,UI 绝不用线性。
长列表错峰如何不等 5 秒?
错峰上限 5-8 项。超过则整体淡入容器而非逐项级联——100 项 × 50ms = 5s 才出现最后一项。
动画时间应定义在哪?
运动令牌(持续 150/250/400ms、缓动曲线)全应用共享。每个动画引用 motion.normal,绝不用魔法数字——保持时间一致。