motion-foundations

motion-foundations

熱門

使用 motion/react 建構 React / Next.js 的動效 Token、彈簧預設值、效能規範、裝置適應、無障礙強制執行與 SSR 安全機制。此為基礎層級——所有其他動效 Skill 皆依賴此 Skill。

24萬星標
3.6萬分支
更新於 2026/8/3
SKILL.md
唯讀
名稱
motion-foundations
描述

使用 motion/react 建構 React / Next.js 的動效 Token、彈簧預設值、效能規範、裝置適應、無障礙強制執行與 SSR 安全機制。此為基礎層級——所有其他動效 Skill 皆依賴此 Skill。

版本
1.0

Motion Foundations

動效系統的基礎層級。定義了下游 Skill(motion-patternsmotion-advanced)所繼承的所有數值、約束與規則。在開始進行任何動畫工作前,請先載入此 Skill。

啟用時機

  • 從零開始建構任何動畫元件
  • 設定 Token、彈簧預設值或 easing 數值
  • 實作 prefers-reduced-motion 支援
  • 除錯動畫初始狀態造成的 Hydration 不一致(mismatch)
  • 評估某個動畫是否真的有必要存在

產出項目

此 Skill 會產出:

  • 共用的 motionTokens 物件(duration、easing、distance、scale)
  • 共用的 springs 預設值對照表(5 個具名設定)
  • 所有元件皆使用的 shouldAnimate() 閘門
  • 透過 useReducedMotion 符合無障礙規範的動畫預設值
  • 零 Hydration 警告的 SSR 安全初始狀態

原則

動效必須至少達到以下其中一項效益,否則就應該移除:

  • 引導注意力
  • 傳達狀態
  • 維持空間連貫性

響應速度的優先度永遠高於流暢度。一個達到 60 fps 但會造成輸入延遲的動畫,比沒有動畫還要糟糕。

規則

這些規則不可妥協,適用於系統中的每一個元件。

  1. 僅限使用 motion/react 切勿從 framer-motion 匯入。切勿在同一個元件樹中混用兩者。
  2. initial 必須與伺服器端渲染結果一致。 若伺服器渲染 opacity: 1,則 initial prop 也必須是 opacity: 1。絕無例外。
  3. 無障礙減速 (Reduced motion) 優先權高於一切。useReducedMotion() 傳回 trueprefersReducedtrue 時,停用所有 transform 變形。唯一允許的備用方案是 ≤ 0.2s 的純透明度淡入淡出(opacity-only fades)。
  4. 絕不對 Layout 屬性做動畫。 animate 中禁止使用 widthheighttopleftmarginpadding。只能使用 transformopacity
  5. 所有 Token 數值必須來自 motionTokens 禁止在元件檔案中寫死 (hardcode) 動畫持續時間 (duration) 和 easing。
  6. 所有彈簧設定必須來自 springs 對照表。 禁止內聯 (inline) stiffness/damping 數值。
  7. 所有從 motion/react 匯入的檔案皆必須加上 "use client"
  8. 絕不可在模組頂層讀取 windownavigator 一律使用 typeof window !== "undefined" 進行防護。

決策指南

選擇持續時間 (Duration)

Token 使用時機
instant Tooltip 顯示/隱藏、Focus 外框環 (focus ring)、Badge 更新
fast 按鈕回饋、圖示切換、Chip 切換
normal Modal 開啟、Card 展開、頁面元素進入
slow Hero 主視覺登場、全頁面轉場
crawl 刻意營造的視覺敘事;請謹慎使用

選擇彈簧設定 (Spring)

預設值 使用時機
snappy 預設 UI —— 按鈕、Chip、導覽項目
gentle 卡片、Modal、面板輕柔落位
bouncy 活潑互動時刻 —— 空狀態 (empty states)、新手引導 (onboarding)
instant Tooltip、Popover、下拉選單 (dropdown)
release 拖曳釋放 —— 自然物理質感

何時應完全停用動畫

在以下情況時停用(讓 shouldAnimate() 傳回 false):

  • prefersReducedtrue
  • isLowEndtrue 且該動畫非核心必要
  • 元素處於螢幕外且永遠不會進入視埠 (viewport)
  • 動畫純屬裝飾,無 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
  },
}

無障礙規範

優先順序(由高至低):

  1. prefers-reduced-motion: reduce —— 停用所有 transform 變形,並將透明度轉場限制在 ≤ 0.2s
  2. 低階裝置偵測 —— 縮短持續時間,移除非核心必要的動畫
  3. 設計偏好 —— 其他所有情況

動效必須優雅降級 (degrade gracefully),絕不可突兀消失而造成版面跳動 (layout shift) 或干擾視覺方向感。

// 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 安全機制

規則:initial 必須永遠與伺服器渲染內容一致。

// 錯誤 —— 伺服器渲染 opacity:1 但 initial 寫 0 → 導致 hydration mismatch
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} />

// 正確 —— 使用 AnimatePresence 或延遲至客戶端掛載 (mount)
"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>
  )
}

限制與非目標

此 Skill 不涵蓋 以下內容:

  • UI 元件模式(按鈕、Modal、交錯/stagger)→ 請參閱 motion-patterns
  • 拖曳、手勢、SVG、文字動畫、自訂 Hook → 請參閱 motion-advanced
  • 純 CSS 動畫或未配合 motion/react 的 Tailwind animate-* class
  • 第三方動畫庫(GSAP、anime.js 等)
  • 動效設計決策(何時使用動畫、該強調何者)—— 此屬設計範疇而非程式碼限制

反模式

反模式 違反規則 修正方式
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 序列與自訂 Hook。