SKILL.md
唯讀
名稱
motion-foundations
描述
使用 motion/react 建構 React / Next.js 的動效 Token、彈簧預設值、效能規範、裝置適應、無障礙強制執行與 SSR 安全機制。此為基礎層級——所有其他動效 Skill 皆依賴此 Skill。
版本
1.0
Motion Foundations
動效系統的基礎層級。定義了下游 Skill(motion-patterns、motion-advanced)所繼承的所有數值、約束與規則。在開始進行任何動畫工作前,請先載入此 Skill。
啟用時機
- 從零開始建構任何動畫元件
- 設定 Token、彈簧預設值或 easing 數值
- 實作
prefers-reduced-motion支援 - 除錯動畫初始狀態造成的 Hydration 不一致(mismatch)
- 評估某個動畫是否真的有必要存在
產出項目
此 Skill 會產出:
- 共用的
motionTokens物件(duration、easing、distance、scale) - 共用的
springs預設值對照表(5 個具名設定) - 所有元件皆使用的
shouldAnimate()閘門 - 透過
useReducedMotion符合無障礙規範的動畫預設值 - 零 Hydration 警告的 SSR 安全初始狀態
原則
動效必須至少達到以下其中一項效益,否則就應該移除:
- 引導注意力
- 傳達狀態
- 維持空間連貫性
響應速度的優先度永遠高於流暢度。一個達到 60 fps 但會造成輸入延遲的動畫,比沒有動畫還要糟糕。
規則
這些規則不可妥協,適用於系統中的每一個元件。
- 僅限使用
motion/react。 切勿從framer-motion匯入。切勿在同一個元件樹中混用兩者。 initial必須與伺服器端渲染結果一致。 若伺服器渲染opacity: 1,則initialprop 也必須是opacity: 1。絕無例外。- 無障礙減速 (Reduced motion) 優先權高於一切。 當
useReducedMotion()傳回true或prefersReduced為true時,停用所有 transform 變形。唯一允許的備用方案是 ≤ 0.2s 的純透明度淡入淡出(opacity-only fades)。 - 絕不對 Layout 屬性做動畫。
animate中禁止使用width、height、top、left、margin、padding。只能使用transform與opacity。 - 所有 Token 數值必須來自
motionTokens。 禁止在元件檔案中寫死 (hardcode) 動畫持續時間 (duration) 和 easing。 - 所有彈簧設定必須來自
springs對照表。 禁止內聯 (inline)stiffness/damping數值。 - 所有從
motion/react匯入的檔案皆必須加上"use client"。 - 絕不可在模組頂層讀取
window或navigator。 一律使用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):
prefersReduced為trueisLowEnd為true且該動畫非核心必要- 元素處於螢幕外且永遠不會進入視埠 (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
},
}
無障礙規範
優先順序(由高至低):
prefers-reduced-motion: reduce—— 停用所有 transform 變形,並將透明度轉場限制在 ≤ 0.2s- 低階裝置偵測 —— 縮短持續時間,移除非核心必要的動畫
- 設計偏好 —— 其他所有情況
動效必須優雅降級 (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的 Tailwindanimate-*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。






