选项

motion-foundations

affaan-m/ECC affaan-m/ECC

使用 motion/react 实现 React / Next.js 的运动令牌、弹出预设、性能规则、设备适配、无障碍规范执行以及 SSR 安全性。基础层——所有其他运动相关技能都以此为基础。

...展开全部
51
更新时间 2026-07-29

运动基础

运动系统的基层。定义了所有值、约束和 规则,这些内容将被下游技能(motion-patterns, motion-advanced)所继承的每个值、约束和 规则。 在开始任何动画工作之前,请加载此技能。

何时激活

  • 从零开始创建任何带动画的组件时
  • 设置标记、弹簧预设或缓动值
  • 实现 prefers-reduced-motion 支持
  • 调试动画初始状态的加载不匹配问题
  • 评估动画是否应存在

输出

该技能生成:

  • 一个共享的 motionTokens 对象(持续时间、缓动、距离、缩放)
  • 一个共享的 springs 预设映射(5个命名配置)
  • 一个 shouldAnimate() 所有组件共用的门控
  • 通过 useReducedMotion
  • SSR安全初始状态,且无数据加载警告

原则

动画必须满足以下条件之一,否则必须移除:

  • 引导注意力
  • 传达状态
  • 保持空间连续性

响应性始终优先于流畅性。如果60帧/秒的动画会导致 输入延迟,那么它还不如没有动画。

规则

这些规则不容商榷。它们适用于系统中的每个组件。

  1. 仅使用motion/react。切勿从 framer-motion。切勿在同一树中混合使用这两者。
  2. initial 必须与服务器输出一致。如果服务器渲染 opacity: 1,则 initial prop 也必须是 opacity: 1。绝无例外。
  3. 运动缩减优先于一切。当 useReducedMotion() 返回 true 或 prefersReduced 为 true时,所有变换均被禁用。仅允许在 ≤ 0.2 秒内进行纯不透明度淡入淡出作为备选方案。
  4. 切勿对布局属性进行动画处理。 width, height, top, left, margin, padding 被禁止用于 animate。请使用 transform 和 opacity 。
  5. 所有令牌值均来自motionTokens。严禁在组件文件中硬编码持续时间和缓动效果。
  6. 所有弹簧配置均来自 springs 映射。禁止使用内联 stiffness/damping 值均被禁止。
  7. "use client" 在每个从 motion/react.
  8. 切勿在模块级别读取 window 或 navigator。请始终使用 typeof window !== "undefined".

决策指南

选择持续时间

选择弹簧

何时应完全禁用动画

禁用(使 shouldAnimate() 返回 false)的情况:

  • prefersReduced 是 true
  • isLowEnd 且 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
  },
}

无障碍

优先级顺序(从高到低):

  1. prefers-reduced-motion: reduce — 禁用所有变换,将不透明度过渡时间限制在 ≤ 0.2 秒
  2. 低端设备检测 — 缩短时长,移除非必要的动画
  3. 设计偏好 — 其余所有情况

动画必须平滑降级。绝不能以导致 布局偏移或混淆方向的方式突然消失。

// 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 序列和自定义钩子。
在 GitHub 上查看

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-motion support
  • Debugging hydration mismatches from animation initial states
  • Evaluating whether an animation should exist at all

Outputs

This skill produces:

  • A shared motionTokens object (duration, easing, distance, scale)
  • A shared springs preset 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.

  1. Use motion/react only. Never import from framer-motion. Never mix the two in the same tree.
  2. initial must match server output. If the server renders opacity: 1, the initial prop must also be opacity: 1. No exceptions.
  3. Reduced motion overrides everything. When useReducedMotion() returns true or prefersReduced is true, all transforms are disabled. Opacity-only fades at ≤ 0.2s are the only permitted fallback.
  4. Never animate layout properties. width, height, top, left, margin, padding are banned from animate. Use transform and opacity only.
  5. All token values come from motionTokens. Hardcoded durations and easings in component files are forbidden.
  6. All spring configs come from the springs map. Inline stiffness/damping values are forbidden.
  7. "use client" is required on every file that imports from motion/react.
  8. Never read window or navigator at module level. Always guard with typeof window !== "undefined".

Decision Guidance

Choosing a duration

Choosing a spring

When to disable animation entirely

Disable (make shouldAnimate() return false) when:

  • prefersReduced is true
  • isLowEnd is true and 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):

  1. prefers-reduced-motion: reduce — disables all transforms, limits opacity transitions to ≤ 0.2s
  2. Low-end device detection — reduces duration, removes non-essential animations
  3. 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 without motion/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. Adds useAnimate sequences and custom hooks on top of this foundation.

所有文件

1 个文件

安装 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/

复制 复制
快速设置: 将技能文件夹复制到 .claude/skills/ 目录下,Claude 会自动检测并使用该技能
仓库 affaan-m/ECC

相关技能

web-search
更新时间 2026-06-29
computer-use
更新时间 2026-07-29
webapp-testing
更新时间 2026-06-29
lark-base
更新时间 2026-07-05
OR