关于 interaction-design
通过动效、反馈和状态转换设计界面交互的技能,面向 UI 设计者与开发者,提供加载状态、手势等交互的实现指导,并涵盖无障碍与性能考量。
主要功能
- 01微交互与反馈模式
- 02页面和组件转换
- 03加载状态与进度指示
- 04手势及拖放交互指导
- 05减少动态效果与性能实践
适用场景
- 01为按钮和控件添加反馈
- 02设计加载与进度状态
- 03构建滑动或拖放交互
- 04规划无障碍页面转换
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ wshobson/ agents --skill interaction-design安装后在 Claude Code 中输入/interaction-design确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/animation-libraries.mdmicrointeraction-patterns.mdscroll-animations.md详细介绍
交互设计
通过动效、反馈和周到的状态过渡,创建引人入胜、直观的交互,从而提升可用性并让用户感到愉悦。
何时使用此技能
- 添加微交互以增强用户反馈
- 实现流畅的页面和组件过渡
- 设计加载状态和骨架屏
- 创建基于手势的交互
- 构建通知和轻提示系统
- 实现拖放界面
- 添加滚动触发动画
- 设计悬停和聚焦状态
核心原则
1. 有目的的动效
动效应当用于传达信息,而非装饰:
- 反馈:确认用户操作已发生
- 方向:展示元素的来源和去向
- 聚焦:将注意力引导至重要变化
- 连续性:在过渡期间保持上下文
2. 时长指南
| 时长 | 用例 |
|---|---|
| 100-150ms | 微反馈(悬停、点击) |
| 200-300ms | 小型过渡(开关、下拉菜单) |
| 300-500ms | 中型过渡(模态框、页面切换) |
| 500ms+ | 复杂的编排动画 |
3. 缓动函数
/* Common easings */
--ease-out: cubic-bezier(0.16, 1, 0.3, 1); /* Decelerate - entering */
--ease-in: cubic-bezier(0.55, 0, 1, 0.45); /* Accelerate - exiting */
--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); /* Both - moving between */
--spring: cubic-bezier(0.34, 1.56, 0.64, 1); /* Overshoot - playful */
快速开始:按钮微交互
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);
}
无障碍考量
/* Respect user motion preferences */
@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进行优化
评论
支持 Markdown