动画第16周笔记-CSS transition vs Motion 库 + stagger 入场 + reduced-motion

路线图阶段:第三阶段(Design Engineer 工具箱)第16周 —— 动画(Motion 库,原 framer-motion) 任务:换主色时的颜色平滑过渡(CSS) + 色卡 stagger 刷新入场(Motion) + 尊重 prefers-reduced-motion(a11y) 核心收获:先穷尽便宜工具再上库;动画永远不能让用户等;key-based remounting;一个属性只让一套系统管

⚠️ 守住的 scope:只做一个 Motion 动效(色卡 stagger)。PreviewCard 保持纯 CSS 过渡 —— 两种技术并存本身就是好示范。


0. ⭐ 第一课:CSS transition 和动画库的边界

项目里早就有动画(hover:scale-105 transition)—— 那库解决什么 CSS 做不了的?

CSS transition 做不了(或很痛苦)的四类:

1. enter/exit    元素"出现/消失"的动画(exit 尤其难:DOM 移除是瞬间的,来不及播离场)
2. layout        位置/尺寸因布局变化而移动时平滑过渡(列表重排)
3. 手势驱动       拖拽(drag)、跟手
4. 编排(orchestration) 多元素按顺序/错峰(stagger)入场

一句话边界:“状态 A↔B 的样式渐变”用 CSS transition;“出现/消失/重排/编排”才上库。 方法论:先穷尽便宜工具,再上贵的。 本周颜色过渡用 CSS 一行解决,只有 stagger 编排才装库。 (同 week11 “静态 Tailwind / 动态行内”——先问需求属于哪侧,再选工具。)


1. 好的 UI 动画服务于三件事(设计判断框架)

  1. 反馈(feedback):告诉用户”操作生效了”(例:已复制)
  2. 连续性(continuity):帮用户理解”什么变成了什么”(例:换色时颜色流动)
  3. 引导(orientation):告诉用户”该看哪里”

本周判断:换色跳变突兀 → 加过渡(continuity) ✓;复制反馈文字已够 → 不加(克制) ✓。 知道哪里不加,是 motion design 的一半功力。


2. ⭐ 动效时长:1000ms 的教训

行业基准(Material / Apple HIG 收敛值):

100-200ms   微反馈(hover、按下)
200-400ms   常规过渡(颜色/位置/展开) ← sweet spot
400-700ms   大型转场(整页切换)
1000ms+     仅叙事性动画(开屏、庆祝)

想给 PreviewCard 用 1000ms —— 被否,理由:

最终:全部统一 duration-300

动效调试技巧

先夸张(duration-1000 + 大反差颜色 #C16952→#3B82F6)确认机制生效,再收回合理值。 大反差 + 慢速度 = 确认生效;小变化 + 快速度 = 什么都看不出。


3. CSS 颜色过渡:transition-colors 加在哪

规则:加在”颜色实际发生变化的那个元素”上。 父容器加了不会让子元素跟着平滑——每个元素只管自己的属性。


4. Motion 库(原 framer-motion,2024 底改名)

npm install motion        # NOT framer-motion(旧名,老教程都是它,别照抄)
import { motion } from "motion/react";   // new import path

装完重启 dev server。报 Failed to resolve import = 依赖/路径层问题(没装/装错名/没重启), 排查顺序:npm list <包名> → 拼写 → 重启。resolve 错误 100% 不在组件代码里。

核心 API:motion.xxx + 三件套

<motion.button
  initial={{ opacity: 0, scale: 0.92 }}   // start state (before appearing)
  animate={{ opacity: 1, scale: 1 }}      // end state
  transition={{ duration: 0.3 }}           // how: duration/easing/delay
>

Figma 翻译:initial = 原型起始状态,animate = 结束状态,transition = Smart Animate 的时长曲线。 声明式(declarative):描述”从哪到哪”,库负责补间(tweening)。 motion.button 依然是 button —— a11y 成果(Tab/Enter/focus ring)全保留。


5. ⭐⭐ key-based remounting:让”数据变化”触发入场动画

问题:initial/animate 是挂载(mount)时的入场动画。但换主色时色卡不会重新挂载——React 靠 key 判断”同一个元素”(week3 reconciliation),role 没变就复用实例,动画不重播。

解法:key 里掺入”每次换色都变”的数据:

key={`${role}-${primaryHex}`}   // primary color changes → key changes → remount → animation replays

⭐ 为什么用 primaryHex 而不是 timestamp

原则:key 用”能代表元素身份的数据”,不用人造触发器。 这是 React 社区的标准手法(key-based remounting)。


6. stagger 错峰:index → delay

// parent: .map() 的第二个参数就是 index
{colorsArray.map(([role, color], index) => (
  <Swatch index={index} ... />

// Swatch: transition 里按序递增 delay
transition={{ duration: 0.3, delay: index * 0.05 }}

7. ⭐ 冲突:一个属性只让一套系统管

animate={{ scale: 1 }}          // Motion 管 scale(行内 style 写 transform)
className="hover:scale-105"     // Tailwind hover 也管 scale → 抢同一个 transform!

Motion 行内样式优先级高 → 动画播完留下的 scale:1 可能压住 hover:scale-105。

原则:一个属性只让一套系统管(transform 归 Motion,颜色/阴影归 CSS)。 和”一个数据一个源头”同一思想,用在动画上。


8. ⭐ prefers-reduced-motion(动画的 a11y 义务)

前庭障碍/晕动症用户会开系统”减少动态效果”——位移/缩放类动画会让他们头晕恶心。尊重它是义务。

import { motion, useReducedMotion } from "motion/react";

const shouldReduceMotion = useReducedMotion();   // true if user prefers reduced motion
<motion.button
  initial={shouldReduceMotion ? false : { opacity: 0, scale: 0.92 }}
  // initial={false} → skip enter animation entirely, appear at final state

⭐ initial={false} 不是”假”,是哨兵值(sentinel value)

范围判断:真正要关的是位移/缩放/视差这类”动”的东西(致晕);颜色渐变温和不致晕,一般不关。只处理 Motion 这边,别过度。

验证:macOS 系统设置 → 辅助功能 → 显示 → 减弱动态效果,切换确认动画跳过/恢复。真测过,不是想当然。


第16周验收


反复提醒过、需继续巩固的点