motion-framer について
motion-framer は、React や JavaScript でアニメーション付き UI を作る開発者向けに、Motion と Framer Motion を解説するスキルです。ジェスチャー、レイアウト・退出アニメーション、スプリング効果、フックや実装例を扱います。
主な機能
- 01motion コンポーネントによる宣言的なアニメーション
- 02ホバー・タップ・ドラッグ操作への対応
- 03レイアウト変更と退出アニメーション
- 04スプリング物理とスクロールアニメーション
- 05アニメーション用フックとバリアントの解説
活用シーン
- 01ボタンやメニューへの操作効果の追加
- 02ページ遷移やスクロール演出の作成
- 03ドラッグ&ドロップやレイアウト変更の実装
- 04状態に応じて動く UI の構築
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ freshtechbro/ claudedesignskills --skill motion-framerインストール後、Claude Code で/motion-framerを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
assets/examples/README.mdassets/starter_motion/index.htmlpackage.jsonREADME.mdvite.config.jsassets/starter_motion/src/App.cssApp.jsxmain.jsxassets/starter_motion/src/components/DraggableBox.jsxHoverCard.jsxStaggerList.jsxreferences/api_reference.mdscripts/animation_generator.pyvariant_builder.py詳細
Motion と Framer Motion
概要
Motion(旧称 Framer Motion)は、React と JavaScript 向けの本番環境に対応したアニメーションライブラリで、最小限のコードで宣言的かつ高パフォーマンスなアニメーションを実現します。HTML 要素をアニメーション対応にする motion コンポーネントを提供し、ジェスチャー認識(ホバー、タップ、ドラッグ、フォーカス)をサポートするほか、レイアウトアニメーション、終了時のアニメーション、ばね物理などの高度な機能も備えています。
このスキルを使用する場面:
- インタラクティブな UI コンポーネント(ボタン、カード、メニュー)の構築
- マイクロインタラクションやホバーエフェクトの作成
- ページ遷移やルートアニメーションの実装
- スクロールベースのアニメーションやパララックス効果の追加
- レイアウト変更(サイズ変更、並べ替え、共有要素の遷移)のアニメーション化
- ドラッグ&ドロップインターフェイス
- 複雑なアニメーションシーケンスや状態ベースのアニメーション
- CSS トランジションを、より強力で制御しやすいアニメーションに置き換える
技術:
- Motion(v11 以降) - Framer Motion の開発者による、よりモダンで軽量なライブラリ
- Framer Motion - 機能が充実した旧ライブラリ(現在も広く使用されています)
- React 18 以降に対応し、Vue もサポート
- TypeScript に対応
- Next.js、Vite、Remix、および最新の React フレームワークすべてで動作
基本概念
1. Motion コンポーネント
motion. を付けることで、あらゆる HTML/SVG 要素をアニメーション可能なコンポーネントに変換できます:
import { motion } from "framer-motion"
// Regular HTML becomes motion component
<motion.div />
<motion.button />
<motion.svg />
<motion.path />
すべての motion コンポーネントは、animate、initial、transition などのアニメーション用 props と、whileHover、whileTap などのジェスチャー用 props を受け取ります。
2. Animate Prop
animate prop はアニメーションの目標状態を定義します。値が変わると、Motion は自動的にその状態へアニメーションします:
// Simple animation - x position changes
<motion.div animate={{ x: 100 }} />
// Multiple properties
<motion.div animate={{ x: 100, opacity: 1, scale: 1.2 }} />
// Animates when state changes
const [isOpen, setIsOpen] = useState(false)
<motion.div animate={{ width: isOpen ? 300 : 100 }} />
3. 初期状態
initial prop を使って、アニメーション開始前の初期状態を設定します:
<motion.div
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
/>
initial={false} を設定すると、マウント時の初期アニメーションを無効にできます。
4. トランジション
transition prop を使って、状態間のアニメーションの動きを制御します:
// Duration-based
<motion.div
animate={{ x: 100 }}
transition={{ duration: 0.5, ease: "easeInOut" }}
/>
// Spring physics
<motion.div
animate={{ scale: 1.2 }}
transition={{ type: "spring", stiffness: 300, damping: 20 }}
/>
// Different transitions for different properties
<motion.div
animate={{ x: 100, opacity: 1 }}
transition={{
x: { type: "spring", stiffness: 300 },
opacity: { duration: 0.2 }
}}
/>
トランジションの種類:
"tween"(デフォルト) - イージングを使った時間ベースのアニメーション"spring"- 物理ベースのばねアニメーション"inertia"- 減速するアニメーション(ドラッグで使用)
5. バリアント
名前付きバリアントを使ってアニメーション状態を整理すると、コードがすっきりし、子要素への伝播も可能になります:
const variants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
exit: { opacity: 0, scale: 0.9 }
}
<motion.div
variants={variants}
initial="hidden"
animate="visible"
exit="exit"
/>
バリアントの伝播 - 子要素は親のバリアント状態を自動的に継承します:
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1 // Stagger child animations
}
}
}
const itemVariants = {
hidden: { x: -20, opacity: 0 },
visible: { x: 0, opacity: 1 }
}
<motion.ul variants={containerVariants} initial="hidden" animate="visible">
<motion.li variants={itemVariants} />
<motion.li variants={itemVariants} />
<motion.li variants={itemVariants} />
</motion.ul>
よく使われるパターン
1. ホバーアニメーション
whileHover prop を使って、ホバー時にアニメーションします:
// Simple hover effect
<motion.button
whileHover={{ scale: 1.1 }}
transition={{ duration: 0.2 }}
>
Hover me
</motion.button>
// Multiple properties
<motion.div
whileHover={{
scale: 1.05,
backgroundColor: "#f0f0f0",
boxShadow: "0px 10px 30px rgba(0, 0, 0, 0.2)"
}}
>
Hover card
</motion.div>
// With custom transition
<motion.button
whileHover={{
scale: 1.2,
transition: { duration: 0.1 } // Transition for gesture start
}}
transition={{ duration: 0.5 }} // Transition for gesture end
>
Button
</motion.button>
ネストした要素を含むホバー:
<motion.div whileHover="hover" variants={cardVariants}>
<motion.h3 variants={titleVariants}>Title</motion.h3>
<motion.img variants={imageVariants} />
</motion.div>
2. タップ/押下時のアニメーション
whileTap prop を使って、タップ/押下時にアニメーションします:
// Scale down on tap
<motion.button
whileTap={{ scale: 0.9 }}
>
Click me
</motion.button>
// Combined hover + tap
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95, rotate: 3 }}
>
Interactive button
</motion.button>
// With variants
const buttonVariants = {
rest: { scale: 1 },
hover: { scale: 1.1 },
pressed: { scale: 0.95 }
}
<motion.button
variants={buttonVariants}
initial="rest"
whileHover="hover"
whileTap="pressed"
>
Button
</motion.button>
3. ドラッグ操作
drag prop を使って、要素をドラッグ可能にします:
// Basic dragging (both axes)
<motion.div drag />
// Constrain to axis
<motion.div drag="x" /> // Only horizontal
<motion.div drag="y" /> // Only vertical
// Drag constraints
<motion.div
drag
dragConstraints={{ left: -100, right: 100, top: -100, bottom: 100 }}
/>
// Drag with parent constraints
<motion.div ref={constraintsRef}>
<motion.div drag dragConstraints={constraintsRef} />
</motion.div>
// Visual feedback while dragging
<motion.div
drag
whileDrag={{
scale: 1.1,
boxShadow: "0px 10px 20px rgba(0,0,0,0.2)",
cursor: "grabbing"
}}
dragElastic={0.1} // Elasticity when dragging outside constraints
dragTransition={{ bounceStiffness: 600, bounceDamping: 20 }}
/>
ドラッグイベント:
<motion.div
drag
onDragStart={(event, info) => console.log(info.point)}
onDrag={(event, info) => console.log(info.offset)}
onDragEnd={(event, info) => console.log(info.velocity)}
/>
4. 終了時のアニメーション(AnimatePresence)
AnimatePresence を使うと、コンポーネントが DOM から削除されるときにアニメーションできます:
import { AnimatePresence } from "framer-motion"
// Basic exit animation
<AnimatePresence>
{isVisible && (
<motion.div
key="modal"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
/>
)}
</AnimatePresence>
主な要件:
- コンポーネントは
<AnimatePresence>の直接の子要素である必要があります - 一意の
keyprop が必要です - 終了時のアニメーションを定義するには
exitprop を使用します
終了時のアニメーションを含むリスト項目:
<AnimatePresence>
{items.map(item => (
<motion.li
key={item.id}
initial={{ opacity: 0, x: -50 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 50 }}
layout // Smooth layout shifts
>
{item.name}
</motion.li>
))}
</AnimatePresence>
順次再生する終了時のアニメーション:
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
when: "beforeChildren",
staggerChildren: 0.1
}
},
exit: {
opacity: 0,
transition: {
when: "afterChildren",
staggerChildren: 0.05,
staggerDirection: -1 // Reverse order
}
}
}
<AnimatePresence>
{show && (
<motion.div variants={containerVariants} initial="hidden" animate="visible" exit="exit">
<motion.div variants={itemVariants} />
<motion.div variants={itemVariants} />
<motion.div variants={itemVariants} />
</motion.div>
)}
</AnimatePresence>
5. レイアウトアニメーション
layout prop を使うと、レイアウトの変更(位置、サイズ)を自動的にアニメーションできます:
// Animate all layout changes
<motion.div layout />
// Animate only position changes
<motion.div layout="position" />
// Animate only size changes
<motion.div layout="size" />
グリッドレイアウトのアニメーション:
const [columns, setColumns] = useState(3)
<motion.div className="grid">
{items.map(item => (
<motion.div
key={item.id}
layout
transition={{ layout: { duration: 0.3, ease: "easeInOut" } }}
/>
))}
</motion.div>
共有レイアウトアニメーション(layoutId):
layoutId を使って異なる 2 つの要素をつなぎ、滑らかな遷移を実現します:
// Tab indicator example
<nav>
{tabs.map(tab => (
<button key={tab.id} onClick={() => setActive(tab.id)}>
{tab.label}
{activeTab === tab.id && (
<motion.div
layoutId="underline"
style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 2 }}
/>
)}
</button>
))}
</nav>
// Modal opening from thumbnail
<motion.img
src={thumbnail}
layoutId="product-image"
onClick={() => setExpanded(true)}
/>
<AnimatePresence>
{expanded && (
<motion.div layoutId="product-image">
<img src={fullsize} />
</motion.div>
)}
</AnimatePresence>
6. スクロールベースのアニメーション
whileInView を使うと、要素がビューポートに入ったときにアニメーションできます:
<motion.div
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, amount: 0.8 }} // once: trigger once, amount: 80% visible
transition={{ duration: 0.5 }}
>
Animates when scrolled into view
</motion.div>
ビューポートのオプション:
once: true- アニメーションは一度だけ開始しますamount: 0.5- 表示される要素の割合(0~1)、または "some" | "all"margin: "-100px"- ビューポート境界のオフセット
順次再生するスクロールアニメーション:
<motion.ul
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.3 }}
variants={{
visible: {
opacity: 1,
transition: { staggerChildren: 0.1 }
},
hidden: { opacity: 0 }
}}
>
<motion.li variants={itemVariants} />
<motion.li variants={itemVariants} />
<motion.li variants={itemVariants} />
</motion.ul>
7. ばねアニメーション
ばね物理を使って、自然で弾むようなアニメーションを実現します:
// Basic spring
<motion.div
animate={{ scale: 1.2 }}
transition={{ type: "spring" }}
/>
// Customize spring physics
<motion.div
animate={{ x: 100 }}
transition={{
type: "spring",
stiffness: 300, // Higher = faster, snappier (default: 100)
damping: 20, // Higher = less bouncy (default: 10)
mass: 1, // Higher = more inertia (default: 1)
}}
/>
// Visual duration (easier spring control)
<motion.div
animate={{ rotate: 90 }}
transition={{
type: "spring",
visualDuration: 0.5, // Perceived duration
bounce: 0.25 // Bounciness (0-1, default: 0.25)
}}
/>
ばねのプリセット:
- Gentle:
stiffness: 100, damping: 20 - Wobbly:
stiffness: 200, damping: 10 - Stiff:
stiffness: 400, damping: 30 - Slow:
stiffness: 50, damping: 20
ジェスチャー認識
Motion は宣言的なジェスチャーハンドラーを提供します:
ジェスチャーのプロパティ
<motion.div
whileHover={{ scale: 1.1 }} // Pointer hovers over element
whileTap={{ scale: 0.9 }} // Primary pointer presses element
whileFocus={{ outline: "2px" }} // Element gains focus
whileDrag={{ scale: 1.1 }} // Element is being dragged
whileInView={{ opacity: 1 }} // Element is in viewport
/>
ジェスチャーイベント
<motion.div
onHoverStart={(event, info) => {}}
onHoverEnd={(event, info) => {}}
onTap={(event, info) => {}}
onTapStart={(event, info) => {}}
onTapCancel={(event, info) => {}}
onDragStart={(event, info) => {}}
onDrag={(event, info) => {}}
onDragEnd={(event, info) => {}}
onViewportEnter={(entry) => {}}
onViewportLeave={(entry) => {}}
/>
イベント情報オブジェクトには次の内容が含まれます:
point: { x, y }- ページ上の座標offset: { x, y }- ドラッグ開始位置からのオフセットvelocity: { x, y }- ドラッグの速度
フック
useAnimate
useAnimate フックを使ってアニメーションを手動で制御します:
import { useAnimate } from "framer-motion"
function Component() {
const [scope, animate] = useAnimate()
useEffect(() => {
// Animate multiple elements
animate([
[scope.current, { opacity: 1 }],
["li", { x: 0, opacity: 1 }, { delay: stagger(0.1) }],
[".button", { scale: 1.2 }]
])
}, [])
return (
<div ref={scope}>
<ul>
<li>Item 1</li>
<li>Item 2</li>
</ul>
<button className="button">Click</button>
</div>
)
}
アニメーションコントロール:
const controls = animate(element, { x: 100 })
controls.play()
controls.pause()
controls.stop()
controls.speed = 0.5
controls.time = 0 // Seek to start
useSpring
スプリングアニメーションのモーション値を作成します:
import { useSpring } from "framer-motion"
function Component() {
const x = useSpring(0, { stiffness: 300, damping: 20 })
return (
<motion.div style={{ x }}>
<button onClick={() => x.set(100)}>Move</button>
</motion.div>
)
}
useInView
要素がビューポート内にあるかどうかを検出します:
import { useInView } from "framer-motion"
function Component() {
const ref = useRef(null)
const isInView = useInView(ref, { once: true, amount: 0.5 })
return (
<div ref={ref}>
{isInView ? "In view!" : "Not in view"}
</div>
)
}
統合パターン
GSAP との連携
Motion を状態ベースのアニメーションに、GSAP を複雑なタイムラインに使用します:
import { motion } from "framer-motion"
import gsap from "gsap"
function Component() {
const boxRef = useRef()
const handleClick = () => {
// Use GSAP for complex timeline
const tl = gsap.timeline()
tl.to(boxRef.current, { rotation: 360, duration: 1 })
.to(boxRef.current, { scale: 1.5, duration: 0.5 })
}
return (
// Use Motion for hover/tap/layout animations
<motion.div
ref={boxRef}
whileHover={{ scale: 1.1 }}
onClick={handleClick}
/>
)
}
React Three Fiber との連携
Motion の値を使って 3D オブジェクトをアニメーション化します:
import { motion } from "framer-motion"
import { useFrame } from "@react-three/fiber"
function Box() {
const x = useMotionValue(0)
useFrame(() => {
// Sync Motion value with Three.js position
meshRef.current.position.x = x.get()
})
return (
<>
<mesh ref={meshRef}>
<boxGeometry />
<meshStandardMaterial />
</mesh>
<motion.div
style={{ x }}
drag="x"
dragConstraints={{ left: -5, right: 5 }}
/>
</>
)
}
フォームライブラリとの連携
フォームの検証状態をアニメーション化します:
import { motion, AnimatePresence } from "framer-motion"
function FormField({ error }) {
return (
<div>
<motion.input
animate={{
borderColor: error ? "#ff0000" : "#cccccc",
x: error ? [0, -10, 10, -10, 10, 0] : 0 // Shake animation
}}
transition={{ duration: 0.4 }}
/>
<AnimatePresence>
{error && (
<motion.p
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
style={{ color: "#ff0000" }}
>
{error}
</motion.p>
)}
</AnimatePresence>
</div>
)
}
パフォーマンスの最適化
1. transform プロパティを使用する
transform プロパティ(x、y、scale、rotate)はハードウェアアクセラレーションに対応しています:
// ✅ Good - Hardware accelerated
<motion.div animate={{ x: 100, scale: 1.2 }} />
// ❌ Avoid - Triggers layout/paint
<motion.div animate={{ left: 100, width: 200 }} />
2. 個別の transform プロパティ
Motion は、コードを簡潔にするための個別の transform プロパティに対応しています:
// Individual properties (Motion feature)
<motion.div style={{ x: 100, rotate: 45, scale: 1.2 }} />
// Traditional (also supported)
<motion.div style={{ transform: "translateX(100px) rotate(45deg) scale(1.2)" }} />
3. アクセシビリティのためにモーションを減らす
モーションを減らすユーザー設定を尊重します:
import { useReducedMotion } from "framer-motion"
function Component() {
const shouldReduceMotion = useReducedMotion()
return (
<motion.div
animate={{ x: 100 }}
transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.5 }}
/>
)
}
4. レイアウトアニメーションのパフォーマンス
レイアウトアニメーションは処理負荷が高くなることがあります。次の方法で最適化します:
// Specify what to animate
<motion.div layout="position" /> // Only position, not size
// Optimize transition
<motion.div
layout
transition={{
layout: { duration: 0.3, ease: "easeOut" }
}}
/>
5. layoutId の使用を控える
layoutId は共有レイアウトアニメーションを作成しますが、要素をグローバルに追跡します。必要な場合にのみ使用してください。
よくある落とし穴
1. 終了アニメーションで AnimatePresence を忘れる
問題: 終了アニメーションが機能しません
// ❌ Wrong - No AnimatePresence
{show && <motion.div exit={{ opacity: 0 }} />}
// ✅ Correct - Wrapped in AnimatePresence
<AnimatePresence>
{show && <motion.div exit={{ opacity: 0 }} />}
</AnimatePresence>
2. リストで key プロパティがない
問題: AnimatePresence が要素を追跡できません
// ❌ Wrong - No key
<AnimatePresence>
{items.map(item => <motion.div exit={{ opacity: 0 }} />)}
</AnimatePresence>
// ✅ Correct - Unique keys
<AnimatePresence>
{items.map(item => (
<motion.div key={item.id} exit={{ opacity: 0 }} />
))}
</AnimatePresence>
3. transform 以外のプロパティをアニメーション化する
問題: カクつくアニメーション、低いパフォーマンス
// ❌ Avoid - Not hardware accelerated
<motion.div animate={{ top: 100, left: 50, width: 200 }} />
// ✅ Better - Use transforms
<motion.div animate={{ x: 50, y: 100, scaleX: 2 }} />
4. レイアウトアニメーションの使いすぎ
問題: レイアウトアニメーションを適用した要素が多い場合、パフォーマンスに問題が生じます
// ❌ Too many layout animations
{items.map(item => <motion.div layout>{item}</motion.div>)}
// ✅ Use layout only where needed, optimize others
{items.map(item => (
<motion.div
key={item.id}
animate={{ opacity: 1 }} // Cheaper animation
exit={{ opacity: 0 }}
/>
))}
5. 複雑なアニメーションで Variants を使用しない
問題: アニメーションコードが重複し、子要素を連携して制御できません
// ❌ Repetitive
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} />
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} />
// ✅ Use variants
const variants = {
hidden: { opacity: 0 },
visible: { opacity: 1 }
}
<motion.div variants={variants} initial="hidden" animate="visible" />
<motion.div variants={variants} initial="hidden" animate="visible" />
6. トランジションのタイミングが不適切
問題: 特定のジェスチャーにトランジションが適用されません
// ❌ Wrong - General transition won't apply to whileHover
<motion.div
whileHover={{ scale: 1.2 }}
transition={{ duration: 1 }} // This applies to animate prop, not whileHover
/>
// ✅ Correct - Transition in whileHover or separate gesture transition
<motion.div
whileHover={{
scale: 1.2,
transition: { duration: 0.2 } // Applies to hover start
}}
transition={{ duration: 0.5 }} // Applies to hover end
/>
リソース
公式ドキュメント
- Motion Docs - Motion の公式ドキュメント
- Framer Motion Docs - Framer Motion(旧版)
- Motion GitHub - ソースコードとサンプル
同梱リソース
このスキルには次の内容が含まれています:
references/
api_reference.md- Motion API の完全なリファレンスvariants_patterns.md- Variant のパターンとオーケストレーションgesture_guide.md- ジェスチャー処理の包括的なガイド
scripts/
animation_generator.py- Motion コンポーネントのボイラープレートを生成variant_builder.py- Variant の設定を行うインタラクティブツール
assets/
starter_motion/- Motion + Vite の完全なスターターテンプレートexamples/- 実際のプロジェクトで使える Motion コンポーネントのパターン
コミュニティリソース
- Motion Dev Discord - 公式コミュニティ
- Framer Motion Examples - インタラクティブなサンプル
- Motion Recipes - よく使われるパターン
- CodeSandbox Templates - ライブデモ