framer-motion について
Framer Motion 12.x を使う React 開発者向けの Web 専用アニメーションスキルです。宣言的なアニメーション、ジェスチャー、トランジション、動きを抑えるアクセシビリティ対応を扱います。
主な機能
- 01宣言的なモーションコンポーネント
- 02複雑なシーケンス向けのバリアント
- 03ホバー、タップ、ドラッグのジェスチャー
- 04ページとレイアウトのトランジション
- 05スクロール連動アニメーション
- 06動きを抑える設定への対応
活用シーン
- 01React の操作要素へのアニメーション追加
- 02コンポーネント間のアニメーション連携
- 03ページや状態の切り替えへのトランジション追加
- 04動きを抑える設定への配慮
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ dralgorhythm/ claude-agentic-framework --skill framer-motionインストール後、Claude Code で/framer-motionを入力して読み込まれたか確認します。
詳細
Framer Motion
プラットフォーム: Web のみ。モバイルアニメーションについては、react-native-reanimated スキルを参照してください。
概要
Framer Motion 12.x を使用した React のアニメーションパターンです。宣言的なアニメーション、ジェスチャー処理、レイアウトのトランジション、ページアニメーションを、パフォーマンスとアクセシビリティを組み込んだ形で提供します。
インストール:pnpm add framer-motion
ワークフロー
アニメーションの追加:
- motion コンポーネントをインポートします:
import { motion } from 'framer-motion' - 要素を motion バリアントに置き換えます:
<div>→<motion.div> - アニメーションの props を追加します:initial、animate、transition
- 視差効果を減らす設定を有効にしてテストします
- DevTools で 60fps のパフォーマンスを確認します
複雑なシーケンス:
- 名前付き状態を持つ variants オブジェクトを定義します
- 親と子に variants を適用します
- orchestration の props を使用します:staggerChildren、delayChildren
- アンマウントする場合は AnimatePresence で囲みます
- アクセシビリティのフォールバックを追加します
アニメーションのプリミティブ
基本的な Motion コンポーネント
import { motion } from 'framer-motion';
// Simple fade-in
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
>
Content
</motion.div>
// Slide up with fade
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, ease: 'easeOut' }}
>
Content
</motion.div>
複雑なアニメーションのための Variants
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
delayChildren: 0.2
}
},
exit: { opacity: 0, transition: { duration: 0.15 } }
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
exit: { opacity: 0, y: -20 }
};
<motion.ul variants={containerVariants} initial="hidden" animate="visible" exit="exit">
{items.map(item => (
<motion.li key={item.id} variants={itemVariants}>
{item.name}
</motion.li>
))}
</motion.ul>
トランジション
標準のタイミング
// Use consistent timing across app
const timing = {
fast: 0.15, // Micro-interactions
normal: 0.3, // Default animations
slow: 0.5, // Page transitions
stagger: 0.05 // Between items
};
// Duration and easing
<motion.div
animate={{ x: 100 }}
transition={{ duration: timing.normal, ease: 'easeInOut' }}
/>
// Spring physics (preferred for natural motion)
<motion.div
animate={{ scale: 1.2 }}
transition={{ type: 'spring', stiffness: 300, damping: 20 }}
/>
// Keyframes
<motion.div
animate={{ scale: [1, 1.2, 1] }}
transition={{ duration: 0.5, times: [0, 0.5, 1] }}
/>
// Repeat
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 2, repeat: Infinity, repeatType: 'loop' }}
/>
ジェスチャー
ホバー、タップ、フォーカス
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
whileFocus={{ outline: '2px solid blue' }}
transition={{ duration: 0.15 }}
>
Click me
</motion.button>
// Complex hover state
<motion.div
initial="rest"
whileHover="hover"
variants={{
rest: { scale: 1, boxShadow: '0 2px 4px rgba(0,0,0,0.1)' },
hover: { scale: 1.02, boxShadow: '0 8px 16px rgba(0,0,0,0.15)' }
}}
>
Card content
</motion.div>
制約付きドラッグ
import { useRef } from 'react';
const constraintsRef = useRef(null);
<div ref={constraintsRef} style={{ width: 400, height: 400 }}>
<motion.div
drag
dragConstraints={constraintsRef}
dragElastic={0.1}
whileDrag={{ scale: 1.1, cursor: 'grabbing' }}
>
Drag me
</motion.div>
</div>
// Drag along single axis
<motion.div drag="x" dragConstraints={{ left: -100, right: 100 }}>
Slide horizontal
</motion.div>
レイアウトアニメーション
自動レイアウトアニメーション
// Auto-animates position/size changes
<motion.div layout>
{expanded ? <FullContent /> : <Summary />}
</motion.div>
// Shared element transitions
<motion.div layoutId="card-123">
<motion.img layoutId="card-image-123" src={image} />
</motion.div>
// Coordinate sibling animations
import { LayoutGroup } from 'framer-motion';
<LayoutGroup>
{items.map(item => (
<motion.div key={item.id} layout>
{item.content}
</motion.div>
))}
</LayoutGroup>
ページトランジション
終了アニメーションのための AnimatePresence
import { AnimatePresence } from 'framer-motion';
// Single element
<AnimatePresence mode="wait">
{isVisible && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
Content
</motion.div>
)}
</AnimatePresence>
// Route transitions (with React Router)
import { useLocation } from 'react-router-dom';
const location = useLocation();
<AnimatePresence mode="wait" initial={false}>
<motion.div
key={location.pathname}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
transition={{ duration: 0.3 }}
>
<Routes location={location}>
{/* routes */}
</Routes>
</motion.div>
</AnimatePresence>
スタガーパターン
// Parent orchestration
const listVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
delayChildren: 0.2
}
}
};
const itemVariants = {
hidden: { opacity: 0, x: -20 },
visible: { opacity: 1, x: 0 }
};
<motion.ul variants={listVariants} initial="hidden" animate="visible">
{items.map(item => (
<motion.li key={item.id} variants={itemVariants}>
{item.name}
</motion.li>
))}
</motion.ul>
// Custom stagger with useAnimate
import { useAnimate, stagger } from 'framer-motion';
const [scope, animate] = useAnimate();
useEffect(() => {
animate('.item', { opacity: 1 }, { delay: stagger(0.05) });
}, []);
パフォーマンス
GPU アクセラレーション対応プロパティ
// ✅ FAST: Only transform and opacity
<motion.div
animate={{
opacity: 1,
scale: 1.2,
x: 100,
rotate: 45
}}
/>
// ❌ SLOW: Layout-affecting properties
<motion.div
animate={{
width: 300, // Triggers layout
height: 200, // Triggers layout
top: 50 // Triggers layout
}}
/>
willChange による最適化
// Hint browser before expensive animations
<motion.div
style={{ willChange: 'transform' }}
whileHover={{ scale: 1.1 }}
>
Content
</motion.div>
// Auto willChange with layout animations
<motion.div layout transition={{ layout: { duration: 0.3 } }}>
Content
</motion.div>
アクセシビリティ
視差効果を減らす設定への対応
import { useReducedMotion } from 'framer-motion';
function AnimatedComponent() {
const shouldReduceMotion = useReducedMotion();
return (
<motion.div
initial={{ opacity: 0, y: shouldReduceMotion ? 0 : 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: shouldReduceMotion ? 0 : 0.3 }}
>
Content
</motion.div>
);
}
// Disable animations completely
const prefersReducedMotion = useReducedMotion();
<motion.div
{...(prefersReducedMotion ? {} : {
initial: { opacity: 0 },
animate: { opacity: 1 },
transition: { duration: 0.3 }
})}
>
Content
</motion.div>
フォーカス管理
// Maintain focus during animations
<AnimatePresence>
{isOpen && (
<motion.dialog
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
onAnimationComplete={() => {
// Focus first input after enter animation
dialogRef.current?.querySelector('input')?.focus();
}}
>
<form>...</form>
</motion.dialog>
)}
</AnimatePresence>
スクロールアニメーション
useScroll と useInView
import { motion, useScroll, useTransform, useInView } from 'framer-motion';
import { useRef } from 'react';
// Scroll progress indicator
function ScrollProgress() {
const { scrollYProgress } = useScroll();
return (
<motion.div
className="fixed top-0 left-0 right-0 h-1 bg-blue-600 origin-left"
style={{ scaleX: scrollYProgress }}
/>
);
}
// Parallax effect
function ParallaxSection() {
const ref = useRef(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start end", "end start"]
});
const y = useTransform(scrollYProgress, [0, 1], [100, -100]);
return (
<div ref={ref}>
<motion.div style={{ y }}>
Parallax content
</motion.div>
</div>
);
}
// Trigger animation when element enters viewport
function AnimateOnScroll({ children }: { children: React.ReactNode }) {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: "-100px" });
return (
<motion.div
ref={ref}
initial={{ opacity: 0, y: 50 }}
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 50 }}
transition={{ duration: 0.5 }}
>
{children}
</motion.div>
);
}
MotionConfig
グローバルアニメーション設定
import { MotionConfig } from 'framer-motion';
// Apply global settings to all descendants
function App() {
return (
<MotionConfig
reducedMotion="user" // Respect prefers-reduced-motion
transition={{ duration: 0.3, ease: "easeOut" }}
>
<YourApp />
</MotionConfig>
);
}
// Override transitions for a section
function FastSection() {
return (
<MotionConfig transition={{ duration: 0.15 }}>
<motion.div animate={{ scale: 1.1 }}>
Uses fast transition
</motion.div>
</MotionConfig>
);
}
ベストプラクティス
- 複雑な複数ステップのアニメーションには、インラインオブジェクトではなく variants を使用します
- 自然な動きには、時間ベースのイージングより スプリング物理を優先します
- 60fps のパフォーマンスを実現するため、transform と opacity のみをアニメーションさせます
- 視差効果を減らす設定を 必ず有効にしてテストします(システム設定 → アクセシビリティ)
- ルートや状態間で共有要素のトランジションを行うには layoutId を使用します
- 終了アニメーションは、一意のキーを指定して AnimatePresence で 必ず囲みます
- アニメーションが頻繁に行われる要素には willChange を設定します
- リストのアニメーションでは、手動で遅延を設定する代わりに staggerChildren を使用します
- 動的でインタラクティブなレイアウトには layout と whileHover を組み合わせます
- 体感性能を保つため、トランジションは 500ms 未満にします
アンチパターン
- ❌ width/height を直接アニメーションさせる(代わりに scale と layout を使用します)
- ❌ 条件付きレンダリングの周囲に AnimatePresence を置き忘れる
- ❌ タイミングの値をハードコードする(定数を使用します)
- ❌ prefers-reduced-motion を無視する
- ❌ GPU 非対応のプロパティ(top、left、width、height、margin)をアニメーションさせる
- ❌ すべての要素に motion を使用する(静的なコンテンツに余計な負荷がかかります)
- ❌ レイアウトアニメーションを深くネストする(パフォーマンスが低下します)
- ❌ AnimatePresence の子要素にキーを指定しない
- ❌ AnimatePresence を使わずに exit を使用する
- ❌ SSR 中にアニメーションさせる(ハイドレーションの不一致が発生します)
フィードバックループ
アニメーションの品質:
# Check frame rate in Chrome DevTools
# Performance → Record → Look for dropped frames
# Target: 60fps (16.67ms per frame)
視差効果を減らす設定のテスト:
# macOS: System Settings → Accessibility → Display → Reduce Motion
# Test all animations with this enabled
パフォーマンスのプロファイリング:
// Use React DevTools Profiler
// Measure commit duration with/without animations
// Aim for <16ms commits
コメント
Markdown 対応