motion-foundations
affaan-m/ECC
使用 motion/react 实现 React / Next.js 的运动令牌、弹出预设、性能规则、设备适配、无障碍规范执行以及 SSR 安全性。基础层——所有其他运动相关技能都以此为基础。
...展开全部运动基础
运动系统的基层。定义了所有值、约束和
规则,这些内容将被下游技能(motion-patterns, motion-advanced)所继承的每个值、约束和
规则。
在开始任何动画工作之前,请加载此技能。
何时激活
- 从零开始创建任何带动画的组件时
- 设置标记、弹簧预设或缓动值
- 实现
prefers-reduced-motion支持 - 调试动画初始状态的加载不匹配问题
- 评估动画是否应存在
输出
该技能生成:
- 一个共享的
motionTokens对象(持续时间、缓动、距离、缩放) - 一个共享的
springs预设映射(5个命名配置) - 一个
shouldAnimate()所有组件共用的门控 - 通过
useReducedMotion - SSR安全初始状态,且无数据加载警告
原则
动画必须满足以下条件之一,否则必须移除:
- 引导注意力
- 传达状态
- 保持空间连续性
响应性始终优先于流畅性。如果60帧/秒的动画会导致 输入延迟,那么它还不如没有动画。
规则
这些规则不容商榷。它们适用于系统中的每个组件。
- 仅使用
motion/react。切勿从framer-motion。切勿在同一树中混合使用这两者。 initial必须与服务器输出一致。如果服务器渲染opacity: 1,则initialprop 也必须是opacity: 1。绝无例外。- 运动缩减优先于一切。当
useReducedMotion()返回true或prefersReduced为true时,所有变换均被禁用。仅允许在 ≤ 0.2 秒内进行纯不透明度淡入淡出作为备选方案。 - 切勿对布局属性进行动画处理。
width,height,top,left,margin,padding被禁止用于animate。请使用transform和opacity。 - 所有令牌值均来自
motionTokens。严禁在组件文件中硬编码持续时间和缓动效果。 - 所有弹簧配置均来自
springs映射。禁止使用内联stiffness/damping值均被禁止。 "use client"在每个从motion/react.- 切勿在模块级别读取
window或navigator。请始终使用typeof window !== "undefined".
决策指南
选择持续时间
选择弹簧
何时应完全禁用动画
禁用(使 shouldAnimate() 返回 false)的情况:
prefersReduced是trueisLowEnd且true且动画非必要时- 该元素位于屏幕外且永远不会进入视口
- 动画仅用于装饰,不涉及用户体验
核心概念
令牌系统
// 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 },
}
运行时标志
// 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
},
}
无障碍
优先级顺序(从高到低):
prefers-reduced-motion: reduce— 禁用所有变换,将不透明度过渡时间限制在 ≤ 0.2 秒- 低端设备检测 — 缩短时长,移除非必要的动画
- 设计偏好 — 其余所有情况
动画必须平滑降级。绝不能以导致 布局偏移或混淆方向的方式突然消失。
// 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; }
}
<div class="motion-safe:animate-fade motion-reduce:opacity-100">div>
SSR / 加载安全
规则:initial 必须始终与服务器渲染的结果一致。
// WRONG — server renders opacity:1 but initial says 0 → hydration mismatch
div initial={{ opacity: 0 }} animate={{ opacity: 1 }} />
// CORRECT — use AnimatePresence or defer to client mount
"use client"
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
div
initial={{ opacity: mounted ? 0 : 1 }}
animate={{ opacity: 1 }}
/>
代码示例
端到端:令牌 + 弹簧 + 可访问性 + 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 guard — initial must match server output (opacity: 1)
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
// Accessibility — disables transform when reduced motion is preferred
const safeMotion = useSafeMotion(motionTokens.distance.md)
// Device gate — skip animation on low-end hardware
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>
)
}
限制 / 非目标
本技能不涵盖:
- UI 组件模式(按钮、模态框、Stagger)→ 参见
motion-patterns - 拖拽、手势、SVG、文本动画、自定义钩子 → 参见
motion-advanced - 纯CSS动画或Tailwind
animate-*类(不包含motion/react - 第三方动画库(GSAP、anime.js 等)
- 动态设计决策(何时添加动画、突出显示什么)——这是设计层面的考量,而非代码限制
反模式
相关技能
motion-patterns— 调用此处定义的令牌和弹簧来构建按钮、模态框、交错、页面过渡和滚动效果。不会重新定义任何值。motion-advanced— 利用此处定义的令牌和弹簧来实现拖拽、SVG、文本和手势效果。在此基础上添加useAnimate序列和自定义钩子。
Motion Foundations
The base layer of the motion system. Defines every value, constraint, and
rule that downstream skills (motion-patterns, motion-advanced) inherit.
Load this skill before any animation work begins.
When to Activate
- Starting any animated component from scratch
- Setting up tokens, spring presets, or easing values
- Implementing
prefers-reduced-motionsupport - Debugging hydration mismatches from animation initial states
- Evaluating whether an animation should exist at all
Outputs
This skill produces:
- A shared
motionTokensobject (duration, easing, distance, scale) - A shared
springspreset map (5 named configs) - A
shouldAnimate()gate used by all components - Accessibility-compliant animation defaults via
useReducedMotion - SSR-safe initial states with zero hydration warnings
Principles
Motion must do at least one of the following or it must be removed:
- Guide attention
- Communicate state
- Preserve spatial continuity
Responsiveness always outranks smoothness. A 60 fps animation that causes input delay is worse than no animation.
Rules
These are non-negotiable. They apply to every component in the system.
- Use
motion/reactonly. Never import fromframer-motion. Never mix the two in the same tree. initialmust match server output. If the server rendersopacity: 1, theinitialprop must also beopacity: 1. No exceptions.- Reduced motion overrides everything. When
useReducedMotion()returnstrueorprefersReducedistrue, all transforms are disabled. Opacity-only fades at ≤ 0.2s are the only permitted fallback. - Never animate layout properties.
width,height,top,left,margin,paddingare banned fromanimate. Usetransformandopacityonly. - All token values come from
motionTokens. Hardcoded durations and easings in component files are forbidden. - All spring configs come from the
springsmap. Inlinestiffness/dampingvalues are forbidden. "use client"is required on every file that imports frommotion/react.- Never read
windowornavigatorat module level. Always guard withtypeof window !== "undefined".
Decision Guidance
Choosing a duration
Choosing a spring
When to disable animation entirely
Disable (make shouldAnimate() return false) when:
prefersReducedistrueisLowEndistrueand the animation is non-essential- The element is off-screen and will never enter the viewport
- The animation is purely decorative with no UX purpose
Core Concepts
Token system
// 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 },
}
Runtime flags
// 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
Priority order (highest to lowest):
prefers-reduced-motion: reduce— disables all transforms, limits opacity transitions to ≤ 0.2s- Low-end device detection — reduces duration, removes non-essential animations
- Design preference — everything else
Motion must degrade gracefully. It must never disappear abruptly in a way that causes layout shift or confuses 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
Rule: initial must always match what the server renders.
// WRONG — server renders opacity:1 but initial says 0 → hydration mismatch
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} />
// CORRECT — use AnimatePresence or defer to client mount
"use client"
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
<motion.div
initial={{ opacity: mounted ? 0 : 1 }}
animate={{ opacity: 1 }}
/>
Code Examples
End-to-end: tokens + springs + accessibility + SSR guard
// 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 guard — initial must match server output (opacity: 1)
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
// Accessibility — disables transform when reduced motion is preferred
const safeMotion = useSafeMotion(motionTokens.distance.md)
// Device gate — skip animation on low-end hardware
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>
)
}
Constraints / Non-Goals
This skill does not cover:
- UI component patterns (button, modal, stagger) → see
motion-patterns - Drag, gestures, SVG, text animations, custom hooks → see
motion-advanced - CSS-only animations or Tailwind
animate-*classes withoutmotion/react - Third-party animation libraries (GSAP, anime.js, etc.)
- Motion design decisions (when to animate, what to emphasize) — that is a design concern, not a code constraint
Anti-Patterns
Related Skills
motion-patterns— consumes tokens and springs defined here to build button, modal, stagger, page transition, and scroll patterns. Does not redefine any values.motion-advanced— consumes tokens and springs defined here for drag, SVG, text, and gesture patterns. AddsuseAnimatesequences and custom hooks on top of this foundation.
安装 motion-foundations
下载技能文件并将其解压到 .claude/skills/ 目录中。
下载ZIP克隆仓库并复制技能文件到您的项目中。
git clone https://github.com/affaan-m/ECC/tree/main/skills/motion-foundations # Copy the skill folder to .claude/skills/ or .codex/skills/
复制





首页
