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 fpsのアニメーションは、 アニメーションがない場合よりも劣る。
ルール
これらは絶対条件です。システム内のすべてのコンポーネントに適用されます。
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() return false)を無効にするタイミング:
prefersReducedがtrueisLowEnd以下の場合trueかつ、アニメーションが必須でない場合- 要素が画面外にあり、ビューポートに入ることはない場合
- アニメーションが純粋に装飾的なものであり、UX上の目的がない場合
基本概念
トークンシステム
// 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コンポーネントのパターン(ボタン、モーダル、スタッガー) → 参照
motion-patterns - ドラッグ、ジェスチャー、SVG、テキストアニメーション、カスタムフック → 参照
motion-advanced - CSSのみのアニメーションやTailwindの
animate-*クラスを使用しないmotion/react - サードパーティ製アニメーションライブラリ(GSAP、anime.jsなど)を使用しないCSSのみのアニメーション
- モーションデザインの決定(いつアニメーションさせるか、何を強調するか) — これはデザインの課題であり、コード上の制約ではない
アンチパターン
関連スキル
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/
コピー





家
