Communitygithub.com

Motion 進階

為 React / Next.js 提供的進階動畫模式 — 拖放、手勢、文字動畫、SVG 路徑繪製、自訂 Hook、指令式序列 (useAnimate)、載入器與完整 API 決策樹。需先安裝 motion-foundations。適用於在 React 或 Next.js 中建置拖放、手勢、文字或 SVG 動畫,或指令式動畫序列。

Motion 進階 是什麼?

Motion 進階 是一套為 React 與 Next.js 開發者設計的進階動畫技能,專注於實作互動性強、基於物理的動畫模式。它能處理從簡單拖曳到複雜手勢、SVG 路徑繪製、文字逐字揭示、自訂動畫 Hook 及指令式序列等所有進階需求。此技能要求先設定好 motion-foundations 基底,並在 motion-patterns 不敷使用時啟用。支援的平台包括 React 和 Next.js,其核心原理強調使用基於物理的動畫(如 useSpring)以達到更自然的互動效果,並利用 useMotionValue 與 useTransform 避免不必要的重新渲染。此外,它還提供詳細的決策指南,協助開發者在不同情境下選擇最合適的 API,例如拖曳、滑動手勢、長按、SVG 變形等,並內建規則確保效能與使用者體驗,例如無限動畫必須在頁面隱藏時暫停,以及自訂 Hook 必須妥善清理事件監聽器。

相容平台~Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/affaan-m/everything-claude-code/tree/main/skills/motion-advanced

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

Motion 進階 是做什麼的?

複雜、互動且基於物理的動畫模式。 需先設定好 motion-foundations。 當 motion-patterns 不夠用時,使用這些模式。

啟用時機

  • 建置拖曳關閉面板、滑動手勢或可重新排序列表
  • 逐字、逐字元動畫文字,或作為即時計數器
  • 繪製 SVG 路徑、變形圖示或動畫化圓形進度
  • 撰寫自訂動畫 Hook(useScrollReveal、磁性按鈕、游標跟隨器)
  • 使用 useAnimate 指令式地排序多步驟動畫
  • 建置載入旋轉器、微光骨架、脈衝指示器或按鈕載入狀態

產出

此技能可產出:

  • 拖曳互動:可拖曳卡片、拖曳關閉面板、Reorder.Group 列表
  • 手勢 Hook:滑動偵測、長按、縮放手勢大綱
  • 文字動畫元件:逐詞揭示、打字機效果、數字計數器
  • SVG 動畫:路徑繪製、圖示變形、筆畫進度環
  • 自訂 Hook:useScrollReveal、useHoverScale、useNavigationDirection、useInViewOnce
  • 使用 useAnimate 的指令式序列,搭配可中斷安全的 async/await
  • 載入器元件:旋轉器、微光、脈衝點、進度條、按鈕載入狀態

原則

  • 基於物理的動畫(useSpring、springs.*)在直接操作時總是比基於持續時間的更自然。
  • useMotionValue + useTransform 可計算衍生值而不觸發重新渲染。
  • useAnimate 序列是指令式且可中斷安全的 — 在中途呼叫 animate() 會自動取消先前的動畫。
  • 運動值(useMotionValue、useSpring)是 SSR 安全的,不會導致水合錯誤。

規則

  1. 拖曳互動必須在觸控裝置上測試,而不僅是滑鼠。drag 屬性在兩者上皆可運作,但手感與閾值不同。
  2. 無限動畫必須在 document.visibilityState === "hidden" 時暫停。 背景分頁不得消耗 GPU/CPU。
  3. 滑動閾值必須明確指定。 切勿僅憑速度推斷意圖;應結合 offset + velocity 檢查。
  4. useAnimate 的作用域 ref 必須附加到已掛載的 DOM 元素上。 在掛載前呼叫 animate() 會靜默失敗。
  5. 運動值不得在渲染時重新建立。 在元件主體內使用 useMotionValue(0) 是正確的;在渲染中使用 new MotionValue(0) 則否。
  6. 所有記號值皆從 motion-foundations 匯入。 不使用內聯數字。
  7. 自訂 Hook 必須處理清理作業。 每個 window.addEventListener 都需要在 useEffect 回傳中有一個對應的 removeEventListener。
  8. SVG 變形需要相等的路徑指令數量。 具有不同指令結構的路徑會產生跳躍而非插值。

決策指引

選擇正確的進階 API

情境API
釋放時帶物理效果的拖曳drag + dragTransition: springs.release
有序拖曳重新排序列表Reorder.Group + Reorder.Item
拖曳偏移關閉drag="y" + onDragEnd 偏移檢查
向左/向右滑動drag="x" + onDragEnd 偏移檢查
長按useLongPress Hook
隨時間平滑的值useSpring
從另一個值衍生useTransform
多步驟序列useAnimate 搭配 async/await
一次性指令式動畫來自 motion 的 animate()
文字逐詞進入在 inline-block 範圍上交錯
SVG 繪製開啟pathLength 從 0 到 1
SVG 變形d 屬性補間(相等指令數)
圓形進度strokeDashoffset 補間

何時使用 useSpring 與彈簧轉場

useSpringtransition: springs.*
用於游標跟隨器、指標追蹤值離散狀態變更
更新連續,每幀更新由狀態變更觸發
中斷平滑 — 物理從速度接續從目前值重新開始

核心概念

useMotionValue + useTransform

無重新渲染的反應式計算:

const x = useMotionValue(0)
const opacity = useTransform(x, [-200, 0, 200], [0, 1, 0])
// opacity 隨 x 變化每幀更新 — 無 setState,無重新渲染

useAnimate

回傳 [scope, animate]。scope ref 必須附加到 DOM 元素上。 animate() 呼叫是可中斷安全的 — 在中途呼叫會取消先前的執行。

const [scope, animate] = useAnimate()

async function play() {
  await animate(".step-1", { opacity: 1 }, { duration: 0.3 })
  await animate(".step-2", { x: 0 },       { duration: 0.4 })
        animate(".step-3", { scale: 1 },    { duration: 0.25 })  // 發射後忘記
}

return <div ref={scope}>...</div>

程式碼範例

可拖曳卡片

"use client"
import { motion } from "motion/react"
import { springs, motionTokens } from "@/lib/motion-tokens"

<motion.div
  drag
  dragConstraints={{ left: -100, right: 100, top: -100, bottom: 100 }}
  dragElastic={0.1}
  whileDrag={{
    scale: motionTokens.scale.pop,
    boxShadow: "0 16px 40px rgba(0,0,0,0.2)",
  }}
  dragTransition={springs.release}
/>

拖曳關閉面板

"use client"
import { motion, useMotionValue, useTransform } from "motion/react"

export function BottomSheet({ onClose }: { onClose: () => void }) {
  const y = useMotionValue(0)
  const opacity = useTransform(y, [0, 200], [1, 0])

  return (
    <motion.div
      drag="y"
      dragConstraints={{ top: 0 }}
      style={{ y, opacity }}
      onDragEnd={(_, info) => {
        // 規則 3:結合 offset + velocity
        if (info.offset.y > 120 || info.velocity.y > 500) onClose()
      }}
    />
  )
}

可重新排序列表

"use client"
import { Reorder } from "motion/react"

export function SortableList() {
  const [items, setItems] = useState(initialItems)
  return (
    <Reorder.Group axis="y" values={items} onReorder={setItems}>
      {items.map((item) => (
        <Reorder.Item key={item.id} value={item}>
          {item.label}
        </Reorder.Item>
      ))}
    </Reorder.Group>
  )
}

滑動偵測

"use client"
import { motion } from "motion/react"

const OFFSET_THRESHOLD  = 50
const VELOCITY_THRESHOLD = 300

<motion.div
  drag="x"
  dragConstraints={{ left: 0, right: 0 }}
  onDragEnd={(_, info) => {
    const swipedRight = info.offset.x > OFFSET_THRESHOLD  || info.velocity.x > VELOCITY_THRESHOLD
    const swipedLeft  = info.offset.x < -OFFSET_THRESHOLD || info.velocity.x < -VELOCITY_THRESHOLD
    if (swipedRight) onSwipeRight()
    if (swipedLeft)  onSwipeLeft()
  }}
/>

長按 Hook

import { useRef } from "react"

export function useLongPress(callback: () => void, ms = 600) {
  const timerRef = useRef<ReturnType<typeof setTimeout>>()
  return {
    onPointerDown:  () => { timerRef.current = setTimeout(callback, ms) },
    onPointerUp:    () => clearTimeout(timerRef.current),
    onPointerLeave: () => clearTimeout(timerRef.current),
  }
}

逐詞揭示

"use client"
import { motion } from "motion/react"
import { springs } from "@/lib/motion-tokens"

export function AnimatedText({ text }: { text: string }) {
  return (
    <motion.p
      variants={{ visible: { transition: { staggerChildren: 0.05 } } }}
      initial="hidden"
      animate="visible"
    >
      {text.split(" ").map((word, i) => (
        <motion.span
          key={i}
          className="inline-block mr-1"
          variants={{
            hidden:  { opacity: 0, y: 12 },
            visible: { opacity: 1, y: 0, transition: springs.gentle },
          }}
        >
          {word}
        </motion.span>
      ))}
    </motion.p>
  )
}

數字計數器

"use client"
import { useRef, useEffect } from "react"
import { animate } from "motion"
import { motionTokens } from "@/lib/motion-tokens"

export function Counter({ to }: { to: number }) {
  const nodeRef = useRef<HTMLSpanElement>(null)

  useEffect(() => {
    const controls = animate(0, to, {
      duration: motionTokens.duration.crawl,
      ease: motionTokens.easing.smooth,
      onUpdate: (v) => {
        if (nodeRef.current) nodeRef.current.textContent = Math.round(v).toString()
      },
    })
    return controls.stop   // 規則 7:清理
  }, [to])

  return <span ref={nodeRef} />
}

SVG 路徑繪製

"use client"
import { motion } from "motion/react"
import { motionTokens } from "@/lib/motion-tokens"

<motion.path
  d="M 0 100 Q 50 0 100 100"
  initial={{ pathLength: 0, opacity: 0 }}
  animate={{ pathLength: 1, opacity: 1 }}
  transition={{ duration: motionTokens.duration.slow, ease: motionTokens.easing.smooth }}
/>

筆畫進度環

"use client"
import { motion } from "motion/react"
import { motionTokens } from "@/lib/motion-tokens"

const CIRCUMFERENCE = 2 * Math.PI * 40   // r=40

export function ProgressRing({ progress }: { progress: number }) {
  return (
    <svg width="100" height="100" viewBox="0 0 100 100">
      <circle cx="50" cy="50" r="40" fill="none" stroke="#e5e7eb" strokeWidth="8" />
      <motion.circle
        cx="50" cy="50" r="40"
        fill="none" stroke="#6366f1" strokeWidth="8"
        strokeLinecap="round"
        strokeDasharray={CIRCUMFERENCE}
        animate={{ strokeDashoffset: CIRCUMFERENCE - (progress / 100) * CIRCUMFERENCE }}
        transition={{ duration: motionTokens.duration.normal, ease: motionTokens.easing.smooth }}
        style={{ rotate: -90, transformOrigin: "center" }}
      />
    </svg>
  )
}

useScrollReveal Hook

"use client"
import { useRef } from "react"
import { useScroll, useTransform } from "motion/react"
import { motionTokens } from "@/lib/motion-tokens"

export function useScrollReveal() {
  const ref = useRef(null)
  const { scrollYProgress } = useScroll({ target: ref, offset: ["start end", "end start"] })
  const opacity = useTransform(scrollYProgress, [0, 0.3], [0, 1])
  const y       = useTransform(scrollYProgress, [0, 0.3], [motionTokens.distance.lg, 0])
  return { ref, style: { opacity, y } }
}

// 使用方式
const { ref, style } = useScrollReveal()
<motion.section ref={ref} style={style} />

游標跟隨器

"use client"
import { useEffect } from "react"
import { motion, useMotionValue, useSpring } from "motion/react"
import { springs } from "@/lib/motion-tokens"

export function CursorFollower() {
  const x = useMotionValue(-100)
  const y = useMotionValue(-100)
  const sx = useSpring(x, springs.gentle)
  const sy = useSpring(y, springs.gentle)

  useEffect(() => {
    const move = (e: MouseEvent) => { x.set(e.clientX); y.set(e.clientY) }
    window.addEventListener("mousemove", move)
    return () => window.removeEventListener("mousemove", move)   // 規則 7
  }, [])

  return (
    <motion.div
      className="fixed top-0 left-0 w-6 h-6 rounded-full bg-indigo-500
                 pointer-events-none -translate-x-1/2 -translate-y-1/2 z-50"
      style={{ x: sx, y: sy }}
    />
  )
}

微光骨架

"use client"
import { useEffect } from "react"
import { motion, useAnimation } from "motion/react"
import { motionTokens } from "@/lib/motion-tokens"

export function ShimmerSkeleton({ className = "" }: { className?: string }) {
  const controls = useAnimation()

  useEffect(() => {
    const play = () =>
      controls.start({
        x: ["-100%", "100%"],
        transition: {
          repeat: Infinity,
          duration: motionTokens.duration.crawl,
          ease: motionTokens.easing.linear,
        },
      })

    const handleVisibility = () => {
      if (document.visibilityState === "hidden") controls.stop()
      else void play()
    }

    void play()
    document.addEventListener("visibilitychange", handleVisibility)
    return () => {
      controls.stop()
      document.removeEventListener("visibilitychange", handleVisibility)
    }
  }, [controls])

  return (
    <div className={`relative overflow-hidden bg-gray-200 rounded ${className}`}>
      <motion.div
        className="absolute inset-0 bg-gradient-to-r from-transparent via-white/60 to-transparent"
        initial={{ x: "-100%" }}
        animate={controls}
      />
    </div>
  )
}

按鈕載入狀態

"use client"
import { motion, AnimatePresence } from "motion/react"
import { motionTokens, springs } from "@/lib/motion-tokens"

export function LoadingButton({
  loading,
  label,
  onClick,
}: {
  loading: boolean
  label: string
  onClick: () => void
}) {
  return (
    <motion.button
      onClick={onClick}
      animate={{ opacity: loading ? 0.7 : 1 }}
      whileTap={loading ? {} : { scale: motionTokens.scale.press }}
      transition={springs.snappy}
      disabled={loading}
    >
      <AnimatePresence mode="wait">
        {loading ? (
          <motion.span
            key="loading"
            initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
            transition={{ duration: motionTokens.duration.fast }}
          >
            …
          </motion.span>
        ) : (
          <motion.span
            key="label"
            initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
            transition={{ duration: motionTokens.duration.fast }}
          >
            {label}
          </motion.span>
        )}
      </AnimatePresence>
    </motion.button>
  )
}

含可見性暫停的無限動畫

"use client"
import { useEffect } from "react"
import { motion, useAnimation } from "motion/react"
import { motionTokens } from "@/lib/motion-tokens"

export function PulseDot() {
  const controls = useAnimation()

  useEffect(() => {
    const pulse = () =>
      controls.start({
        scale: [1, 1.4, 1],
        opacity: [1, 0.6, 1],
        transition: { repeat: Infinity, duration: motionTokens.duration.crawl },
      })

    // 規則 2:分頁隱藏時暫停
    const handleVisibility = () => {
      if (document.visibilityState === "hidden") {
        controls.stop()
      } else {
        void pulse()
      }
    }

    void pulse()
    document.addEventListener("visibilitychange", handleVisibility)
    return () => {
      controls.stop()
      document.removeEventListener("visibilitychange", handleVisibility)
    }
  }, [controls])

  return (
    <motion.div animate={controls} className="w-4 h-4 rounded-full bg-indigo-500" />
  )
}

Magnetic button (可選)

"use client"
import { useRef, useCallback } from "react"
import { motion, useMotionValue, useSpring } from "motion/react"
import { springs } from "@/lib/motion-tokens"

export function MagneticButton({ children }: { children: React.ReactNode }) {
  const ref = useRef<HTMLDivElement>(null)
  const x = useMotionValue(0)
  const y = useMotionValue(0)
  const sx = useSpring(x, springs.gentle)
  const sy = useSpring(y, springs.gentle)


  const handlePointerMove = useCallback((e: React.PointerEvent) => {
    const el = ref.current
    if (!el) return
    const rect = el.getBoundingClientRect()
    const cx = rect.left + rect.width / 2
    const cy = rect.top + rect.height / 2
    const dx = (e.clientX - cx) * 0.3
    const dy = (e.clientY - cy) * 0.3
    x.set(dx)
    y.set(dy)
  }, [x, y])

  const handlePointerLeave = useCallback(() => {
    x.set(0)
    y.set(0)
  }, [x, y])

  return (
    <motion.div
      ref={ref}
      onPointerMove={handlePointerMove}
      onPointerLeave={handlePointerLeave}
      style={{ x: sx, y: sy }}
      className="inline-block"
    >
      {children}
    </motion.div>
  )
}

所有上述範例皆使用來自 motion-foundations 的匯入。 所有自訂 Hook 都透過 useEffect 回傳中的 removeEventListener 清理監聽器。 所有無限動畫都會在背景分頁中暫停。

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/claude-api

Anthropic Claude API patterns for Python and TypeScript. Covers Messages API, streaming, tool use, vision, extended thinking, batches, prompt caching, and Claude Agent SDK. Use when building applications with the Claude API or Anthropic SDKs.

affaan-m/everything-claude-code

End-to-end marketing campaign planning and execution. Covers audience research, positioning, campaign angle definition, landing page copy, email sequences, social posts, ad copy, short-form video scripts, and content calendars. Use as the orchestration layer for multi-channel product launches. Use when planning or executing a multi-channel product launch, or producing landing page, email, social, or ad copy.

affaan-m/everything-claude-code

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/everything-claude-code-conventions

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/frontend-design

Create distinctive, production-grade frontend interfaces with high design quality. Use when the user asks to build web components, pages, or applications and the visual direction matters as much as the code quality.

affaan-m/gget

gget CLI and Python workflow for quick genomic database queries, sequence lookup, BLAST-style searches, enrichment checks, and reproducible bioinformatics evidence logs.

affaan-m/literature-review

Systematic literature-review workflow for academic, biomedical, technical, and scientific topics, including search planning, source screening, synthesis, citation checks, and evidence logging.

affaan-m/motion-ui

Production-ready UI motion system for React/Next.js. Use when implementing animations, transitions, or motion patterns.

affaan-m/project-guidelines-example

Example project-specific skill template based on a real production application.

affaan-m/pubmed-database

Direct PubMed and NCBI E-utilities search workflows for biomedical literature, MeSH queries, PMID lookup, citation retrieval, and API-backed literature monitoring.

affaan-m/scholar-evaluation

Structured scholarly-work evaluation for papers, proposals, literature reviews, methods sections, evidence quality, citation support, and research-writing feedback.

affaan-m/uspto-database

USPTO patent and trademark data workflow for official record lookup, PatentSearch queries, TSDR checks, assignment data, and reproducible IP research logs.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

相關技能