name: interaction-design description: 设计和实现微交互、运动设计、过渡和用户反馈模式。在添加UI交互的抛光、实现加载状态或创建愉悦的用户体验时使用。
交互设计
通过运动、反馈和周到的状态过渡,创建引人入胜、直观的交互,以增强可用性和愉悦用户。
何时使用此技能
- 添加微交互以增强用户反馈
- 实现平滑的页面和组件过渡
- 设计加载状态和骨架屏幕
- 创建基于手势的交互
- 构建通知和吐司系统
- 实现拖放界面
- 添加滚动触发的动画
- 设计悬停和焦点状态
核心原则
1. 有目的的运动
运动应传达信息,而非装饰:
- 反馈:确认用户操作发生
- 定向:显示元素从哪里来/到哪里去
- 焦点:引导注意力到重要变化
- 连续性:在过渡期间保持上下文
2. 时间指南
| 持续时间 | 使用场景 |
|---|---|
| 100-150ms | 微反馈(悬停、点击) |
| 200-300ms | 小过渡(切换、下拉菜单) |
| 300-500ms | 中等过渡(模态框、页面变化) |
| 500ms+ | 复杂编排的动画 |
3. 缓动函数
/* 常见缓动 */
--ease-out: cubic-bezier(0.16, 1, 0.3, 1); /* 减速 - 进入 */
--ease-in: cubic-bezier(0.55, 0, 1, 0.45); /* 加速 - 退出 */
--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); /* 两者 - 移动之间 */
--spring: cubic-bezier(0.34, 1.56, 0.64, 1); /* 过冲 - 趣味性 */
快速开始:按钮微交互
import { motion } from "framer-motion";
export function InteractiveButton({ children, onClick }) {
return (
<motion.button
onClick={onClick}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
className="px-4 py-2 bg-blue-600 text-white rounded-lg"
>
{children}
</motion.button>
);
}
交互模式
1. 加载状态
骨架屏幕:在加载时保持布局
function CardSkeleton() {
return (
<div className="animate-pulse">
<div className="h-48 bg-gray-200 rounded-lg" />
<div className="mt-4 h-4 bg-gray-200 rounded w-3/4" />
<div className="mt-2 h-4 bg-gray-200 rounded w-1/2" />
</div>
);
}
进度指示器:显示确定性进度
function ProgressBar({ progress }: { progress: number }) {
return (
<div className="h-2 bg-gray-200 rounded-full overflow-hidden">
<motion.div
className="h-full bg-blue-600"
initial={{ width: 0 }}
animate={{ width: `${progress}%` }}
transition={{ ease: "easeOut" }}
/>
</div>
);
}
2. 状态过渡
带平滑过渡的切换:
function Toggle({ checked, onChange }) {
return (
<button
role="switch"
aria-checked={checked}
onClick={() => onChange(!checked)}
className={`
relative w-12 h-6 rounded-full transition-colors duration-200
${checked ? "bg-blue-600" : "bg-gray-300"}
`}
>
<motion.span
className="absolute top-1 left-1 w-4 h-4 bg-white rounded-full shadow"
animate={{ x: checked ? 24 : 0 }}
transition={{ type: "spring", stiffness: 500, damping: 30 }}
/>
</button>
);
}
3. 页面过渡
Framer Motion 布局动画:
import { AnimatePresence, motion } from "framer-motion";
function PageTransition({ children, key }) {
return (
<AnimatePresence mode="wait">
<motion.div
key={key}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
>
{children}
</motion.div>
</AnimatePresence>
);
}
4. 反馈模式
点击时的涟漪效果:
function RippleButton({ children, onClick }) {
const [ripples, setRipples] = useState([]);
const handleClick = (e) => {
const rect = e.currentTarget.getBoundingClientRect();
const ripple = {
x: e.clientX - rect.left,
y: e.clientY - rect.top,
id: Date.now(),
};
setRipples((prev) => [...prev, ripple]);
setTimeout(() => {
setRipples((prev) => prev.filter((r) => r.id !== ripple.id));
}, 600);
onClick?.(e);
};
return (
<button onClick={handleClick} className="relative overflow-hidden">
{children}
{ripples.map((ripple) => (
<span
key={ripple.id}
className="absolute bg-white/30 rounded-full animate-ripple"
style={{ left: ripple.x, top: ripple.y }}
/>
))}
</button>
);
}
5. 手势交互
滑动以消除:
function SwipeCard({ children, onDismiss }) {
return (
<motion.div
drag="x"
dragConstraints={{ left: 0, right: 0 }}
onDragEnd={(_, info) => {
if (Math.abs(info.offset.x) > 100) {
onDismiss();
}
}}
className="cursor-grab active:cursor-grabbing"
>
{children}
</motion.div>
);
}
CSS 动画模式
关键帧动画
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.animate-fadeIn {
animation: fadeIn 0.3s ease-out;
}
.animate-pulse {
animation: pulse 2s ease-in-out infinite;
}
.animate-spin {
animation: spin 1s linear infinite;
}
CSS 过渡
.card {
transition:
transform 0.2s ease-out,
box-shadow 0.2s ease-out;
}
.card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1);
}
可访问性考虑
/* 尊重用户运动偏好 */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
function AnimatedComponent() {
const prefersReducedMotion = window.matchMedia(
"(prefers-reduced-motion: reduce)",
).matches;
return (
<motion.div
animate={{ opacity: 1 }}
transition={{ duration: prefersReducedMotion ? 0 : 0.3 }}
/>
);
}
最佳实践
- 性能优先:使用
transform和opacity实现平滑的60fps - 减少运动支持:始终尊重
prefers-reduced-motion - 一致的时间:在应用中使用时间尺度
- 自然物理:优先使用弹簧动画而非线性
- 可中断:允许用户取消长动画
- 渐进增强:在没有JS动画的情况下工作
- 设备测试:性能在不同设备上差异显著
常见问题
- 卡顿动画:避免动画
width、height、top、left - 过度动画:过多运动导致疲劳
- 阻塞交互:在动画期间从不阻止用户输入
- 内存泄漏:在卸载时清理动画监听器
- 内容闪烁:谨慎使用
will-change进行优化