🚀 快速安装

复制以下命令并运行,立即安装此 Skill:

npx skills add https://github.com/wshobson/agents --skill interaction-design

💡 提示:需要 Node.js 和 NPM

交互设计

通过动效、反馈和周到的状态转换,创造引人入胜、直观的交互体验,提升可用性并取悦用户。

何时使用此技能

  • 添加微交互以增强用户反馈
  • 实现流畅的页面和组件过渡
  • 设计加载状态和骨架屏
  • 创建基于手势的交互
  • 构建通知和提示消息系统
  • 实现拖放界面
  • 添加滚动触发的动画
  • 设计悬停和聚焦状态

核心原则

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();  <!-- 滑动超过 100px 触发关闭 -->
        }
      }}
      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 }}  <!-- 用户偏好减少动效则禁用动画 -->
    />
  );
}

最佳实践

  1. 性能优先:使用 transformopacity 实现流畅的 60fps 动画
  2. 支持减少动效:始终尊重 prefers-reduced-motion 媒体查询
  3. 一致的时长:在应用中使用统一的时长比例
  4. 自然的物理效果:优先使用弹簧动画而非线性动画
  5. 可中断:允许用户取消长时间动画
  6. 渐进增强:确保在没有 JS 动画时也能正常工作
  7. 在真机上测试:不同设备性能差异很大

常见问题

  • 卡顿动画:避免对 widthheighttopleft 进行动画
  • 过度动画:动效太多会导致用户疲劳
  • 阻塞交互:切勿在动画期间阻止用户输入
  • 内存泄漏:组件卸载时清理动画监听器
  • 内容闪烁:谨慎使用 will-change 进行优化

📄 原始文档

完整文档(英文):

https://skills.sh/wshobson/agents/interaction-design

💡 提示:点击上方链接查看 skills.sh 原始英文文档,方便对照翻译。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。