Motion Foundations
モーションシステムの基本層。下流のスキル(motion-patterns、motion-advanced)が継承するすべての値、制約、ルールを定義します。アニメーション作業を開始する前にこのスキルをロードしてください。
アクティブにするタイミング
- アニメーションコンポーネントを一から作成する場合
- トークン、スプリングプリセット、イージング値を設定する場合
prefers-reduced-motionサポートを実装する場合- アニメーションの初期状態によるハイドレーションの不一致をデバッグする場合
- アニメーションがそもそも存在すべきかどうかを評価する場合
出力
このスキルは以下を生成します:
- 共有
motionTokensオブジェクト(duration、easing、distance、scale) - 共有
springsプリセットマップ(5つの名前付き設定) - すべてのコンポーネントで使用される
shouldAnimate()ゲート useReducedMotionによるアクセシビリティ準拠のアニメーションデフォルト- ハイドレーション警告がゼロのSSRセーフな初期状態
原則
モーションは以下の少なくとも1つを実行する必要があります。そうでない場合は削除しなければなりません。
- 注意を誘導する
- 状態を伝える
- 空間的連続性を維持する
応答性は常に滑らかさよりも優先されます。入力遅延を引き起こす60fpsのアニメーションは、アニメーションがないよりも悪いです。
ルール
これらは譲歩できません。システム内のすべてのコンポーネントに適用されます。
motion/reactのみを使用すること。framer-motionからインポートしないでください。同じツリー内で2つを混在させないでください。initialはサーバーの出力と一致しなければなりません。 サーバーがopacity: 1をレンダリングする場合、initialプロパティもopacity: 1でなければなりません。例外はありません。- 縮小モーションがすべてを上書きします。
useReducedMotion()がtrueを返すか、prefersReducedがtrueの場合、すべてのトランスフォームが無効になります。不透明度のみのフェード(≤ 0.2秒)のみが許可されたフォールバックです。 - レイアウトプロパティをアニメーション化しないでください。
width、height、top、left、margin、paddingはanimateでの使用が禁止されています。transformとopacityのみを使用してください。 - すべてのトークン値は
motionTokensから取得されます。 コンポーネントファイル内でのハードコードされたdurationやeasingは禁止されています。 - すべてのスプリング設定は
springsマップから取得されます。 インラインのstiffness/damping値は禁止されています。 "use client"は必須です。motion/reactからインポートするすべてのファイルに必要です。- モジュールレベルで
windowやnavigatorを読み取らないでください。 常にtypeof window !== "undefined"でガードしてください。
決定ガイダンス
期間の選択
| トークン | 使用する場合 |
|---|---|
instant | ツールチップの表示/非表示、フォーカスリング、バッジの更新 |
fast | ボタンフィードバック、アイコンの入れ替え、チップのトグル |
normal | モーダルオープン、カード展開、ページ要素の出現 |
slow | ヒーローエントランス、全ページトランジション |
crawl | 意図的なストーリーテリング。控えめに使用 |
スプリングの選択
| プリセット | 使用する場合 |
|---|---|
snappy | デフォルトUI — ボタン、チップ、ナビゲーションアイテム |
gentle | カード、モーダル、パネルがそっと着地する場合 |
bouncy | 遊び心のある瞬間 — 空の状態、オンボーディング |
instant | ツールチップ、ポップオーバー、ドロップダウン |
release | ドラッグリリース — 自然な物理感 |
アニメーションを完全に無効にするタイミング
以下の場合に無効にします(shouldAnimate() が false を返すようにする):
prefersReducedがtrueの場合isLowEndが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; }
}
<!-- Tailwind -->
<div class="motion-safe:animate-fade motion-reduce:opacity-100"></div>
SSR / ハイドレーションの安全性
ルール: initial は常にサーバーがレンダリングするものと一致しなければなりません。
// 間違い — サーバーは opacity:1 をレンダリングするが、initial は 0 を指定 → ハイドレーションの不一致
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} />
// 正しい — AnimatePresence を使用するか、クライアントマウントに延期する
"use client"
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
<motion.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ガード — initial はサーバーの出力 (opacity: 1) と一致する必要がある
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
// アクセシビリティ — 縮小モーションが優先される場合、トランスフォームを無効にする
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>
)
}
制約 / 非目標
このスキルは以下をカバーしません:
- UIコンポーネントパターン(ボタン、モーダル、スタッガー)→
motion-patternsを参照 - ドラッグ、ジェスチャー、SVG、テキストアニメーション、カスタムフック →
motion-advancedを参照 motion/reactを使用しないCSSのみのアニメーションやTailwindのanimate-*クラス- サードパーティのアニメーションライブラリ(GSAP、anime.jsなど)
- モーションデザインの決定(いつアニメーションするか、何を強調するか)— それはコードの制約ではなく、デザインの問題です
アンチパターン
| アンチパターン | 違反するルール | 修正 |
|---|---|---|
import { motion } from "framer-motion" | ルール1 | motion/react を使用する |
SSRコンポーネントでの initial={{ opacity: 0 }} | ルール2 | マウントガードを追加する |
useReducedMotion チェックの省略 | ルール3 | useSafeMotion フックを使用する |
animate={{ width: "100%" }} | ルール4 | 代わりに scaleX トランスフォームを使用する |
インラインの transition={{ duration: 0.4 }} | ルール5 | motionTokens.duration.normal を使用する |
インラインの { stiffness: 300, damping: 30 } | ルール6 | springs.snappy を使用する |
"use client" ディレクティブの欠落 | ルール7 | ファイルの先頭に追加する |
モジュールレベルでの navigator.hardwareConcurrency | ルール8 | typeof navigator !== "undefined" でラップする |
関連スキル
motion-patterns— ここで定義されたトークンとスプリングを使用して、ボタン、モーダル、スタッガー、ページトランジション、スクロールパターンを構築します。値を再定義しません。motion-advanced— ここで定義されたトークンとスプリングを使用して、ドラッグ、SVG、テキスト、ジェスチャーパターンを構築します。この基盤の上にuseAnimateシーケンスとカスタムフックを追加します。