SKILL.md
只读
名称
motion-foundations
描述
基于 motion/react 实现 React / Next.js 的动效 Token、弹簧预设、性能规范、设备适配、无障碍强制约束及 SSR 水合安全保障。这是动效系统的底层基石——所有其他 Motion Skill 均依赖此技能。
版本
1.0
Motion Foundations
动效系统的底层基石。定义了下游 Skill(motion-patterns、motion-advanced)继承的所有数值、约束和规则。在开启任何动画开发工作之前,必须先加载此 Skill。
何时激活
- 从零开始开发任何带有动画效果的组件
- 配置 Token、弹簧预设(spring presets)或缓动值(easing values)
- 实现
prefers-reduced-motion减弱动效支持 - 排查由于动画初始状态导致的 SSR 水合不匹配(hydration mismatch)问题
- 评估某个动画效果是否真的有必要存在
输出产物
此 Skill 包含以下核心产物:
- 共享的
motionTokens对象(包含时长、缓动曲线、距离和缩放比例) - 共享的
springs预设映射表(包含 5 组预设配置) - 通用的
shouldAnimate()动画开关校验方法(供所有组件调用) - 基于
useReducedMotion实现符合无障碍规范的动画默认行为 - 零 Hydration 警告的 SSR 安全初始状态
核心设计理念
动效必须至少满足以下条件之一,否则必须直接移除:
- 引导用户注意力
- 传递状态变化
- 保持空间连续性
响应速度(Responsiveness)永远高于流畅度(Smoothness)。如果一个 60 fps 的动画导致了输入延迟,那它的表现甚至不如没有动画。
强约束规则
以下规则属于不可违背的硬性要求,适用于系统中的每一个组件:
- 仅限使用
motion/react。绝对不要从framer-motion导入。绝对不要在同一个组件树中混合使用两者。 initial状态必须与服务端渲染输出完全一致。如果服务端渲染的是opacity: 1,那么initial属性也必须是opacity: 1,无一例外。- 减弱动效(Reduced Motion)优先于一切。当
useReducedMotion()返回true或prefersReduced为true时,禁用所有 Transform 变形效果。唯一允许的降级方案是时长不超过 0.2s 的仅淡入淡出(Opacity-only fade)。 - 严禁对布局属性(Layout properties)做动画。
width、height、top、left、margin、padding严禁出现在animate中,只能使用transform和opacity。 - 所有 Token 数值必须取自
motionTokens。严禁在组件文件中硬编码(Hardcode)动画时长和缓动参数。 - 所有 Spring 配置必须取自
springs预设映射表。严禁内联写死stiffness/damping参数。 - 必须在所有导入
motion/react的文件顶部添加"use client"指令。 - 严禁在模块层级直接读取
window或navigator。必须始终使用typeof window !== "undefined"进行环境守护防御。
决策指南
选定时长(Duration)
| Token | 适用场景 |
|---|---|
instant |
Tooltip 提示框显示/隐藏、焦点环(Focus ring)、Badge 徽章更新 |
fast |
按钮点击反馈、图标切换、Chip 标签状态切换 |
normal |
弹窗 Modal 打开、卡片展开、页面元素进场 |
slow |
Hero 区域展示动画、整页切换(Full-page transition) |
crawl |
沉浸式叙事动画;请谨慎使用 |
选定弹簧(Spring)
| 预设 | 适用场景 |
|---|---|
snappy |
默认 UI 控件——按钮、Chip 标签、导航项 |
gentle |
卡片、Modal 弹窗、侧边面板的柔和落位动画 |
bouncy |
趣味交互时刻——空状态展示、新用户引导流程 |
instant |
Tooltip 提示、Popover 弹出框、Dropdown 下拉菜单 |
release |
拖拽释放——自然符合物理直觉的手感 |
何时彻底禁用动画
在以下场景中,直接禁用动画(让 shouldAnimate() 返回 false):
prefersReduced为trueisLowEnd为true且该动画属于非核心功能- 元素处于屏幕外(Off-screen)且永远不会进入视口
- 纯装饰性动画,对 UX 提升没有任何实际帮助
核心概念
Token 系统
// lib/motion-tokens.ts
export const motionTokens = {
duration: {
instant: 0.08,
fast: 0.18,
normal: 0.35,
slow: 0.6,
crawl: 1.0,
},
easing: {
smooth: [0.22, 1, 0.36, 1],
sharp: [0.4, 0, 0.2, 1],
bounce: [0.34, 1.56, 0.64, 1],
linear: [0, 0, 1, 1],
},
distance: {
xs: 4,
sm: 8,
md: 16,
lg: 24,
xl: 48,
},
scale: {
subtle: 0.98,
press: 0.95,
pop: 1.04,
},
}
export const springs = {
snappy: { type: "spring", stiffness: 300, damping: 30 },
gentle: { type: "spring", stiffness: 120, damping: 14 },
bouncy: { type: "spring", stiffness: 400, damping: 10 },
instant: { type: "spring", stiffness: 600, damping: 35 },
release: { type: "spring", stiffness: 200, damping: 20, restDelta: 0.001 },
}
运行时 Flag 机制
// lib/motion-config.ts
export const motionConfig = {
isLowEnd() {
return (
typeof navigator !== "undefined" &&
navigator.hardwareConcurrency <= 4
)
},
prefersReduced() {
return (
typeof window !== "undefined" &&
window.matchMedia("(prefers-reduced-motion: reduce)").matches
)
},
shouldAnimate({ essential = false } = {}) {
if (this.prefersReduced()) return false
if (!essential && this.isLowEnd()) return false
return true
},
duration() {
return this.isLowEnd() || this.prefersReduced()
? motionTokens.duration.instant
: motionTokens.duration.normal
},
}
无障碍适配(Accessibility)
优先级顺序(由高到低):
prefers-reduced-motion: reduce—— 禁用所有 Transform 变形,将淡入淡出动画限制在 ≤ 0.2s 内- 低端设备检测 —— 缩短动画时长,移除非核心动画
- 设计偏好 —— 其他所有场景
动效必须具备优雅降级(Degrade gracefully)机制。绝不能出现导致布局偏移(Layout shift)或破坏方位感(Orientation)的猝熄现象。
// hooks/use-reduced-motion.tsx
"use client"
import { useReducedMotion } from "motion/react"
export function useSafeMotion(fullY: number = 16) {
const reduce = useReducedMotion()
return {
initial: { opacity: 0, y: reduce ? 0 : fullY },
animate: { opacity: 1, y: 0 },
exit: { opacity: 0, y: reduce ? 0 : -fullY },
}
}
/* globals.css */
@media (prefers-reduced-motion: reduce) {
.motion-safe-transition { transition: opacity 0.15s; }
.motion-reduce-transform { transform: none !important; }
}
<!-- Tailwind -->
<div class="motion-safe:animate-fade motion-reduce:opacity-100"></div>
SSR / 水合安全(Hydration Safety)
硬规则:initial 的值必须时刻与服务端渲染的输出匹配。
// 错误示例 —— 服务端渲染的是 opacity: 1,但 initial 却写了 0 → 导致水合不匹配(hydration mismatch)
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} />
// 正确示例 —— 使用 AnimatePresence 或在客户端挂载后再触发动画
"use client"
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
<motion.div
initial={{ opacity: mounted ? 0 : 1 }}
animate={{ opacity: 1 }}
/>
代码示例
端到端完整示例:Tokens + Springs + 无障碍适配 + SSR 守护
// components/fade-in-card.tsx
"use client"
import { useState, useEffect } from "react"
import { motion } from "motion/react"
import { motionTokens, springs } from "@/lib/motion-tokens"
import { useSafeMotion } from "@/hooks/use-reduced-motion"
import { motionConfig } from "@/lib/motion-config"
interface FadeInCardProps {
children: React.ReactNode
delay?: number
}
export function FadeInCard({ children, delay = 0 }: FadeInCardProps) {
// SSR 守护 —— initial 必须与服务端渲染输出一致(opacity: 1)
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
// 无障碍适配 —— 开启减弱动效时禁用 transform 变形
const safeMotion = useSafeMotion(motionTokens.distance.md)
// 设备门禁 —— 低端设备上跳过动画渲染
if (!motionConfig.shouldAnimate() || !mounted) {
return <div>{children}</div>
}
return (
<motion.div
initial={safeMotion.initial}
animate={safeMotion.animate}
exit={safeMotion.exit}
transition={{
...springs.gentle,
delay,
}}
whileHover={{ scale: motionTokens.scale.pop }}
whileTap={{ scale: motionTokens.scale.press }}
>
{children}
</motion.div>
)
}
边界与非目标(Non-Goals)
本 Skill 不包含 以下内容:
- UI 组件模式(按钮、Modal、交错动画 stagger)→ 参见
motion-patterns - 拖拽、手势、SVG、文本动画、自定义 Hooks → 参见
motion-advanced - 不依赖
motion/react的纯 CSS 动画或 Tailwindanimate-*类名 - 第三方动画库(GSAP、anime.js 等)
- 动效设计决策(何时做动画、强调重点是什么)—— 这是设计层面的事,而非代码约束
反模式(Anti-Patterns)
| 反模式 | 违反的规则 | 正确做法 |
|---|---|---|
import { motion } from "framer-motion" |
规则 1 | 改用 motion/react |
在 SSR 组件上编写 initial={{ opacity: 0 }} |
规则 2 | 添加挂载守护(Mount guard) |
遗漏 useReducedMotion 无障碍检查 |
规则 3 | 使用 useSafeMotion Hook |
animate={{ width: "100%" }} |
规则 4 | 改用 scaleX Transform 变形 |
内联硬编码 transition={{ duration: 0.4 }} |
规则 5 | 改用 motionTokens.duration.normal |
内联硬编码 { stiffness: 300, damping: 30 } |
规则 6 | 改用 springs.snappy |
缺失 "use client" 指令 |
规则 7 | 在文件顶部加上指令 |
在模块层级直接使用 navigator.hardwareConcurrency |
规则 8 | 使用 typeof navigator !== "undefined" 包裹护航 |
关联 Skill
motion-patterns—— 使用此处定义的 Token 和弹簧预设来构建按钮、Modal 弹窗、交错动画(stagger)、页面过渡及滚动动画模式。不会重复定义任何数值。motion-advanced—— 使用此处定义的 Token 和弹簧预设来实现拖拽、SVG、文本与手势模式。在此基础之上扩展useAnimate动画序列和自定义 Hooks。






