framer-motion について
Claude Code、claude.ai、Cowork 向けのモーションとインタラクション実装スキルです。Motion/Framer Motion の使い方を扱い、依頼がない限り既存プロジェクトのパッケージ名を維持する方針を示します。
主な機能
- 01Motion と Framer Motion の API ガイド
- 02終了・レイアウトアニメーションのパターン
- 03Variants とジェスチャーのガイド
- 04バージョン依存 API の注意点
活用シーン
- 01条件付き UI に終了アニメーションを追加
- 02共有または変化するレイアウトをアニメーション化
- 03プロジェクトで使われているパッケージ名を確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ athevon/ genjutsu --skill framer-motionインストール後、Claude Code で/framer-motionを入力して読み込まれたか確認します。
詳細
バージョン依存の情報です。 以下のすべての API 名、SDK の要件、ブラウザーサポートに関する記述は、一次情報をもとに 2026-09-08 に確認済みです。何をいつ確認したかは
_jutsu/VERSIONS.mdをご覧ください。この日付が古い場合は、バージョン番号に基づいて行動する前に再確認してください。
Framer Motion - サブスキル
パッケージ名は 2 つ、ライブラリは 1 つです。 Framer Motion は Motion に改称されました。
motionとframer-motionはどちらも同じバージョン(2026-09-08 時点で 13.2.0)を公開しています。motionは"framer-motion": "^13.2.0"を依存関係として宣言し、motion/reactはそれを再エクスポートします。どちらも 壊れているわけではなく、インストール済みプロジェクトの大半では、今もframer-motionが使われています。
package.jsonを読み、そこに記載されているものに従ってください。 ユーザーから依頼がない限り、プロジェクトを どちらか一方からもう一方へ移行しないでください。これはcastの鉄則 8、およびpaintの鉄則 10 です。
package.jsonの記載インポート元 インストールコマンド motion"motion/react"npm install motionframer-motion"framer-motion"npm install framer-motion両方 編集中のファイルですでにインポートされている方を使い、プロジェクトが移行途中であることを伝えてください - このスキル内の API はすべて、v13 では 2 つの名前で同一です。v11 より新しいシンボルには、インラインで注記しています。ピア依存関係:
react/react-dom^18 || ^19。
Framer Motion と代替手段の使い分け
| 基準 | Framer Motion | GSAP | ネイティブ CSS |
|---|---|---|---|
| レイアウトアニメーション | 優秀(layoutId) | 手動 | 不可能 |
| 終了アニメーション | AnimatePresence | タイムラインを逆再生 | 限定的(display) |
| ジェスチャー(ドラッグ、ホバー) | ネイティブ、宣言的 | Draggable プラグイン | 基本的 |
| スクロール駆動 | useScroll + useTransform | ScrollTrigger(より高機能) | scroll-timeline |
| 複雑なオーケストレーション | Variants + 伝播 | タイムライン(より柔軟) | @keyframes |
| バンドルサイズ | 約 50kb(ツリーシェイキング後) | コア約 30kb | 0kb |
| React 連携 | ネイティブ、コンポーネント主体 | Refs + useGSAP | className の切り替え |
ルール:モーダル、トースト、並べ替え、共有レイアウトなど、React UI のインタラクションには Framer Motion を使います。複雑なタイムライン、映画のようなスクロール駆動アニメーション、SVG のモーフィングには GSAP を使います。
AnimatePresence - 終了アニメーション
<AnimatePresence mode="wait">
{isVisible && (
<motion.div
key="unique-key" // REQUIRED - identifies the component
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
/>
)}
</AnimatePresence>
mode="wait"- 終了アニメーションの完了後に開始します(ページ遷移)mode="sync"- 終了と開始を同時に行いますmode="popLayout"- ただちにフローから削除します(リストに適しています)onExitComplete- すべての終了アニメーションが完了したときに呼び出されるコールバック
レイアウトアニメーション
// Shared layout - the element "slides" between two positions
<motion.div layoutId="highlight" className={activeTab === id ? "active" : ""} />
// Auto layout - animates position/size when layout changes
<motion.div layout>
{isExpanded && <motion.p layout>Additional content</motion.p>}
</motion.div>
// layout="position" - animates position only (not size)
// layout="size" - animates size only
// layout="preserve-aspect" - preserves the ratio during the transition
Variants - 伝播とオーケストレーション
import { stagger } from "motion/react";
const container = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: {
// staggerChildren + staggerDirection are DEPRECATED (Motion 12.22, Jul 2025).
// Pass a stagger() function to delayChildren instead.
delayChildren: stagger(0.08, { startDelay: 0.2 }),
// reverse order: stagger(0.08, { from: "last" })
// also available: from: "first" | "center" | "last" | index, and ease
},
},
};
const item = {
hidden: { opacity: 0, y: 20 },
show: { opacity: 1, y: 0 },
};
<motion.ul variants={container} initial="hidden" animate="show">
{items.map((i) => (
<motion.li key={i.id} variants={item} />
))}
</motion.ul>
Variants は motion の子要素に自動的に伝播するため、子要素に initial / animate を指定する必要はありません。
ジェスチャー
<motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
whileFocus={{ borderColor: "#3b82f6" }}
// Drag
drag // true = x+y, "x" = horizontal only, "y" = vertical only
dragConstraints={{ left: -100, right: 100, top: -50, bottom: 50 }}
dragElastic={0.2} // 0 = rigid, 1 = free (default 0.35)
dragSnapToOrigin // returns to initial position
onDragEnd={(e, info) => {
if (info.offset.x > 100) handleSwipe("right");
}}
/>
Motion values - 再レンダリングなしのリアクティブな更新
const x = useMotionValue(0);
const opacity = useTransform(x, [-200, 0, 200], [0, 1, 0]);
const background = useTransform(x, [-200, 200], ["#ff0000", "#00ff00"]);
// Spring-based smoothing
const smoothX = useSpring(x, { stiffness: 300, damping: 30 });
// Scroll tracking
const { scrollY, scrollYProgress } = useScroll();
const parallaxY = useTransform(scrollYProgress, [0, 1], [0, -300]);
// Element-scoped scroll
const ref = useRef(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start end", "end start"],
});
Motion values は React の再レンダリングを引き起こしません。style を通じて DOM を直接更新します。
避けるべきこと
isValidProp なしで styled-components / Emotion と motion を使わない(v13 以降)
Motion 13.0 では、オプションの依存関係だった @emotion/is-prop-valid が削除されました。明示的に注入しないと、motion 専用の props が DOM に漏れてしまいます。
import isPropValid from "@emotion/is-prop-valid";
import { MotionConfig } from "motion/react";
<MotionConfig isValidProp={isPropValid}>
<App />
</MotionConfig>
または、スタイルライブラリ側で props の受け渡しを制御するよう、合成順を逆にします:const MotionDiv = motion.create(StyledDiv)。
ガードなしでコールバック内の setState を呼び出さない
// BAD - infinite re-render if animate depends on state
onUpdate={(latest) => setPosition(latest.x)}
// GOOD - guard or useMotionValueEvent
const x = useMotionValue(0);
useMotionValueEvent(x, "change", (latest) => {
if (latest > threshold) onThresholdReached();
});
安定した key なしでレイアウトアニメーションを使わない
// BAD - key changes every render, breaks layout tracking
<motion.div layout key={Math.random()} />
// GOOD - stable key derived from data
<motion.div layout key={item.id} />
AnimatePresence に一意の key を指定し忘れない
// BAD - no key, exit animation does not trigger
<AnimatePresence>
{isOpen && <motion.div exit={{ opacity: 0 }} />}
</AnimatePresence>
// GOOD - unique key for each conditional child
<AnimatePresence>
{isOpen && <motion.div key="modal" exit={{ opacity: 0 }} />}
</AnimatePresence>
すでにアニメーション化されているコンポーネントを motion.div でラップしない
// BAD - double animation, transform conflicts
<motion.div animate={{ x: 100 }}>
<motion.div animate={{ x: -50 }}>Content</motion.div>
</motion.div>
// GOOD - single animation level per transform axis
<motion.div animate={{ x: 100 }}>
<motion.div animate={{ opacity: 0.5 }}>Content</motion.div>
</motion.div>
// GOOD - use variants to coordinate parent/child
<motion.div variants={parent} animate="active">
<motion.div variants={child} />
</motion.div>