Claude CodeにFramer Motionを書かせたら跳ねすぎた話と直し方
Claude CodeでFramer Motion(Motion for React)を実装。motion・variants・AnimatePresenceの使い方と、跳ねすぎ・exit不発を直した実例コードを公開。
「管理画面のカード一覧、いい感じにアニメーションつけといて」
Claude Codeにそう投げて、戻ってきたコードをそのまま貼って、ブラウザを開いた。カードがボヨンボヨン跳ねていました。通知は出るのに消えるアニメーションが効かず、スクロール演出は会社の古いノートPCでカクついた。見た目は派手なのに、実務では全部やり直し。これが僕の最初のFramer Motion体験です。
何が悪かったかというと、僕の頼み方でした。「いい感じ」としか言わなかったから、Claude Codeは「いい感じ=派手」と解釈した。動きの目的も、触っていいファイルの範囲も、低スペック端末のことも、何ひとつ伝えていなかったんです。
この記事では、その失敗を起点に、Framer Motion(公式名 Motion for React)の核になるAPI——motion・variants・AnimatePresence——をClaude Codeにどう書かせ、どう直すかを、貼って動くコード付きで書きます。CSS主体の一般的なアニメーション論ではなく、Framer Motion特有の話に絞ります。
この記事の要点
- 公式名は Motion for React。今は
npm install motion、importはmotion/reactが基本。framer-motionは古い書き方なので新規実装では使わない variantsで「親→子」の動きをまとめると、staggerChildrenで一覧の入場が一気に整う。カード6枚なら遅延0.06〜0.09秒が実用的- 消える要素のアニメーションは
AnimatePresenceの仕事。効かない原因の9割は「親ごと先に消えている」か「keyがindex」のどちらか - スクロール演出は
useScroll+useSpring。ただしuseReducedMotionで動きを切る逃げ道を最初から用意する - Claude Codeへの依頼は「目的・範囲・制約・検証」の4点を必ず書く。これだけで初回の出来が変わる
まずFramer Motionの「今の正解」を押さえる
Framer Motionで検索して情報を集めると、framer-motion からimportするコードが大量に出てきます。これは古い書き方です。2026年時点の公式ドキュメントでは製品名が Motion for React に変わり、インストールは motion、Reactからのimportは motion/react が基本になっています。
# 新規ならこっち。パッケージ名は motion
npm install motion
// 古い書き方(既存プロジェクトに残っていることが多い)
import { motion } from "framer-motion";
// 新しい書き方。新規実装・更新記事ではこちらに統一する
import { motion } from "motion/react";
新規実装ではまず公式のMotion for Reactを基準にする。これだけ先に決めておくと、Claude Codeが古いimportを混ぜてきても一発で気づけます。逆にここを曖昧にすると、同じファイルに framer-motion と motion/react が同居して、レビューのとき「どっちが正?」と毎回手が止まります。
ちなみに「Framer Motionって結局CSSアニメーションと何が違うの」とよく聞かれます。ざっくり言うと、Framer MotionはReactの状態(state)と動きを直結させるためのライブラリです。CSSは「クラスが付いたら動く」。Framer Motionは「このコンポーネントが消えるとき動く」「リストの順番が変わったとき動く」みたいな、Reactのライフサイクルに紐づいた動きが得意。だから状態が激しく変わる管理画面やSPAで効きます。
Claude Codeに渡す依頼は4点セットで決める
僕が「いい感じに」で事故ったのは、依頼が雑だったからです。今は任せる前に必ず次の4点を文章にしてから渡します。
| 観点 | Claude Codeに渡す内容 | レビュー時に見ること |
|---|---|---|
| 目的 | 何を分かりやすくしたいか | 動きが情報理解に貢献しているか |
| 範囲 | 触ってよいファイル・触らないファイル | 余計なリファクタが混ざっていないか |
| 制約 | motion/react、Reduced Motion、既存CSS | 古いimportや過剰な依存がないか |
| 検証 | 手動操作・キーボード・低速端末 | 見た目だけで合格にしていないか |
Claude Codeは「実装者」というより「差分を作る共同作業者」です。設計と検証条件をこっちが先に置くと、出てくるコードの品質がぐっと安定する。流れにするとこんな感じです。
目的と制約
-> Claude Codeへの具体的な依頼
-> Motion for Reactの実装
-> 手動確認とアクセシビリティ確認
-> 必要なら再プロンプト
最初の依頼文は、短くても境界を入れます。僕が実際に使っている雛形がこれです。
Reactの既存コンポーネントにMotion for Reactのアニメーションを追加してください。
条件:
- 新規コードは `motion/react` からimportする
- 既存のデータ取得とフォーム処理は変更しない
- Reduced Motionに対応する
- opacityとtransform中心にして、layout shiftを起こさない
- 変更後に確認する手動テストも箇条書きで出す
この形にすると、Claude Codeが「どこまでやるべきか」を判断しやすくなります。特に既存プロジェクトでは、import元の混在を避ける指示が効きます。
ユースケース1: variants でカード一覧を順番に出す
最初の実例は、ダッシュボードや記事一覧で使うカードの入場アニメーションです。ここで主役になるのが variants という仕組み。動きの状態(hidden / visible など)に名前を付けておき、親で名前を切り替えると子にも伝わる、というFramer Motion独自の書き方です。
variants のうれしいところは、親に staggerChildren を書くだけで子カードが順番に出ること。setTimeout を並べる必要がありません。ただし遅延を長くしすぎると、ユーザーがクリックしたいのにカードがまだ出てこない、という邪魔な演出になります。僕の検証では、カード6枚程度なら0.06〜0.09秒がちょうどよかったです。
import { motion, useReducedMotion } from "motion/react";
type Feature = {
id: string;
title: string;
body: string;
metric: string;
};
const demoFeatures: Feature[] = [
{
id: "review",
title: "レビュー待ち",
body: "Claude Codeが作った差分を人間が確認するキューです。",
metric: "8件",
},
{
id: "motion",
title: "アニメーション改善",
body: "離脱しやすい画面遷移を短い動きで補助します。",
metric: "14%",
},
{
id: "a11y",
title: "Reduced Motion",
body: "動きを減らしたいユーザーにはフェード中心で表示します。",
metric: "対応済み",
},
];
export function AnimatedFeatureCards({
items = demoFeatures,
}: {
items?: Feature[];
}) {
// 端末側で「動きを減らす」設定の人を尊重する
const shouldReduceMotion = useReducedMotion();
// 親の variants。staggerChildren で子を順番に出す
const container = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: shouldReduceMotion ? 0 : 0.08,
},
},
};
// 子カードの variants。Reduced Motion 時は移動量を 0 に
const card = {
hidden: {
opacity: 0,
y: shouldReduceMotion ? 0 : 16,
},
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.32,
ease: "easeOut",
},
},
};
return (
<motion.section
aria-label="機能カード"
variants={container}
initial="hidden"
animate="visible"
style={{
display: "grid",
gap: 16,
gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
}}
>
{items.map((item) => (
<motion.article
key={item.id}
variants={card}
whileHover={shouldReduceMotion ? undefined : { y: -4 }}
style={{
border: "1px solid #d9e2ec",
borderRadius: 12,
padding: 20,
background: "#ffffff",
boxShadow: "0 8px 24px rgba(15, 23, 42, 0.08)",
}}
>
<p style={{ margin: 0, color: "#2563eb", fontWeight: 700 }}>
{item.metric}
</p>
<h3 style={{ margin: "8px 0", fontSize: 18 }}>{item.title}</h3>
<p style={{ margin: 0, lineHeight: 1.7, color: "#475569" }}>
{item.body}
</p>
</motion.article>
))}
</motion.section>
);
}
このコードのレビューで僕が見るのは3点です。key が安定しているか(後述しますが、ここが地味に効きます)、動きが opacity と transform 中心か、そしてReduced Motion時に大きな移動が消えるか。Claude Codeに直しを頼むなら「見た目をもっと豪華に」ではなく「カード数が増えても操作開始を遅らせない範囲で調整して」と言うと、実務向きの数字に寄ります。
ユースケース2: AnimatePresence で通知の追加・削除を破綻させない
冒頭で「通知が消えるアニメーションが効かなかった」と書きました。あれの正体が AnimatePresence の使い方ミスです。
AnimatePresence は、Reactツリーから消える要素に exit アニメーションを与えるためのコンポーネントです。普通のReactは、要素を消すとき問答無用でDOMから引っこ抜きます。だから「消えるときフワッと」をやりたいなら、AnimatePresence で囲んで「消える瞬間をちょっと待って」とお願いする必要がある。公式のAnimatePresenceドキュメントにもある通り、直接の子要素を追跡するので、安定した key が必須です。
Claude Codeが——というか僕が——よくやらかすのは、親コンポーネントごと条件分岐で消してしまい、exit が走る前に全体がアンマウントされるパターン。{showToasts && <Toasts />} みたいに書くとこれが起きます。正解は、リストのコンテナは常に残し、各通知を AnimatePresence の中で増減させること。
import { useState } from "react";
import { AnimatePresence, motion } from "motion/react";
type Toast = {
id: number;
message: string;
};
let nextToastId = 1;
export function AnimatedNotifications() {
const [toasts, setToasts] = useState<Toast[]>([
{ id: 0, message: "保存しました" },
]);
function addToast() {
const id = nextToastId++;
setToasts((current) => [
...current,
{ id, message: `バックグラウンド処理 ${id} が完了しました` },
]);
}
function dismissToast(id: number) {
setToasts((current) => current.filter((toast) => toast.id !== id));
}
return (
<div>
<button type="button" onClick={addToast}>
通知を追加
</button>
{/* コンテナは常に残す。中身だけ AnimatePresence で増減させる */}
<div
aria-live="polite"
style={{
position: "fixed",
right: 24,
top: 24,
display: "grid",
gap: 12,
width: "min(360px, calc(100vw - 48px))",
}}
>
<AnimatePresence initial={false} mode="popLayout">
{toasts.map((toast) => (
<motion.div
layout
key={toast.id}
initial={{ opacity: 0, x: 40, scale: 0.96 }}
animate={{ opacity: 1, x: 0, scale: 1 }}
exit={{ opacity: 0, x: 40, scale: 0.96 }}
transition={{ duration: 0.2, ease: "easeOut" }}
style={{
borderRadius: 10,
border: "1px solid #cbd5e1",
background: "#ffffff",
padding: 16,
boxShadow: "0 12px 30px rgba(15, 23, 42, 0.16)",
}}
>
<p style={{ margin: "0 0 12px", color: "#0f172a" }}>
{toast.message}
</p>
<button type="button" onClick={() => dismissToast(toast.id)}>
閉じる
</button>
</motion.div>
))}
</AnimatePresence>
</div>
</div>
);
}
ポイントは key={toast.id} を必ず安定したIDにすること。ここを配列のindexにすると、要素が増減したとき別物にすり替わって、exit がチカチカ暴れます。あと mode="popLayout" を付けると、1つ消えたとき残りがスッと詰まって気持ちいい。これも framer-motion 時代から名前が変わった部分なので、古いコードを参考にするときは注意です。
Claude Codeに追加で頼むなら「通知が0件のときもコンテナを消さない」「キーボードで閉じられる」「aria-live の読み上げが過剰にならない」の3点。通知は操作中に何度も出るので、見た目が合格でも読み上げとフォーカスが雑だとすぐストレスになります。
ユースケース3: useScroll と useSpring でスクロール演出を作る
LPや長いドキュメントだと、スクロール位置に連動した進捗バーが効きます。ここで使うのがMotionのuseScroll。ページ全体や特定要素の進捗を MotionValue という特殊な値として返してくれます。これを useSpring に通すと、カクカクした進捗がバネみたいに滑らかになる。
useScroll と useSpring はFramer Motion特有の「フックでアニメーション値を扱う」やり方の代表格です。CSSだけだとスクロール量を取って再描画して……と面倒な部分を、フックが裏で効率よく処理してくれます。ただしパララックスを盛りすぎると本文が読みにくくなるので、useReducedMotion が有効なときは位置移動をやめて不透明度だけに寄せます。
import { useRef } from "react";
import {
motion,
useReducedMotion,
useScroll,
useSpring,
useTransform,
} from "motion/react";
const sections = [
{
title: "要件を固定する",
body: "Claude Codeに任せる前に、目的、対象ファイル、禁止事項を明文化します。",
},
{
title: "小さく動かす",
body: "最初はopacityとtransformだけで、ユーザーの理解を助ける動きに絞ります。",
},
{
title: "実機で確認する",
body: "低速端末、キーボード操作、Reduced Motionを確認してから公開します。",
},
];
export function ScrollReadingProgress() {
const articleRef = useRef<HTMLElement | null>(null);
const shouldReduceMotion = useReducedMotion();
// この要素の「読み始め」から「読み終わり」までの進捗を取る
const { scrollYProgress } = useScroll({
target: articleRef,
offset: ["start start", "end end"],
});
// 進捗をバネに通して滑らかにする
const scaleX = useSpring(scrollYProgress, {
stiffness: 120,
damping: 28,
mass: 0.2,
});
// ヘッダーを少しだけ上にずらすパララックス
const y = useTransform(scrollYProgress, [0, 1], [0, -48]);
return (
<article ref={articleRef} style={{ position: "relative", padding: 24 }}>
<motion.div
aria-hidden="true"
style={{
position: "sticky",
top: 0,
zIndex: 10,
height: 4,
// Reduced Motion なら常に満タン表示にして動かさない
scaleX: shouldReduceMotion ? 1 : scaleX,
transformOrigin: "0% 50%",
background: "#2563eb",
}}
/>
<motion.header
style={{
y: shouldReduceMotion ? 0 : y,
padding: "56px 0 32px",
}}
>
<p style={{ color: "#2563eb", fontWeight: 700 }}>
Claude Code x Motion
</p>
<h2 style={{ fontSize: 36, margin: 0 }}>
読み進めるほど文脈が分かるページ
</h2>
</motion.header>
<div style={{ display: "grid", gap: 24 }}>
{sections.map((section) => (
<section
key={section.title}
style={{
border: "1px solid #dbe4ee",
borderRadius: 12,
padding: 24,
background: "#ffffff",
}}
>
<h3>{section.title}</h3>
<p style={{ lineHeight: 1.8 }}>{section.body}</p>
</section>
))}
</div>
</article>
);
}
手順記事やドキュメントサイトでこのパターンを使うなら、進捗バーは「読者の現在地を示す補助」と割り切るのがコツです。セクションが横から飛び込む派手な演出は、最初は楽しくても読み返すと邪魔になる。僕は社内の手順書で一度やって、同僚に「酔う」と言われて消しました。
やらかしやすい落とし穴5つ
Framer Motion特有のハマりどころを、僕が踏んだ順に並べます。
1つ目は、古いimportをそのまま使うこと。既存記事では import { motion } from "framer-motion" が山ほど残っています。すでにそのパッケージで統一されたプロジェクトなら急に混ぜるべきではありませんが、新規実装や更新では npm install motion と motion/react を基準にします。
2つ目は、exit が動かない原因をイージングやdurationだと思い込むこと。多くの場合、犯人は AnimatePresence の外側で親が先に消えていること、またはリストの key がindexになっていること。動きの数字をいじる前に、Reactツリーの残り方を疑ってください。僕はここで丸一日溶かしました。
3つ目は、layout を万能だと思うこと。layout は並び替えやサイズ変化に強い一方、画像の遅延読み込み、CSS Gridの急な列数変更、フォント遅延が重なると意図しない跳ね方になります。Claude Codeには「画像の幅高さを固定する」「カードの最小幅を維持する」などレイアウト側の制約も一緒に頼みます。
4つ目は、アクセシビリティを最後に回すこと。Motion公式のアクセシビリティガイドではReduced Motionへの対応が説明されています。大きな移動・視差効果・自動再生を使うなら、後付けではなく最初の実装時点で useReducedMotion による代替表現を入れます。
5つ目は、パフォーマンス検証を開発PCだけで済ませること。opacity と transform は比較的安全ですが、巨大なぼかし、頻繁な box-shadow、高解像度画像の同時移動は重くなる。僕の「古いノートPCでカクついた」もこれです。Chrome DevToolsのPerformanceだけでなく、実機で実際にスクロールして引っかかりを確認します。
ユースケース4: Claude Codeにレビューまで任せる
実装後の品質は、生成コードそのものよりレビュー依頼の質で決まります。Claude Codeには「批判的に見てほしい観点」を具体的に渡します。
今の差分をMotion for Reactの実装としてレビューしてください。
特に次を厳しく確認してください。
- `motion/react` と古い `framer-motion` のimportが混在していないか
- `AnimatePresence` の直接の子に安定したkeyがあるか
- Reduced Motionで大きな移動、パララックス、自動再生が止まるか
- width/height/top/leftを頻繁に動かしていないか
- 手動確認手順が実際のユーザー操作になっているか
このレビュー依頼を一回挟むだけで、「動くデモ」から「公開できるUI」にぐっと近づきます。Claude Codeは差分全体を見られるので、同じコンポーネント内のCSS・状態管理・テスト不足もまとめて指摘させると効率がいいです。
公開前チェックリスト
僕が毎回見ている項目です。一つでも欠けたら公開しません。
- Motionの公式ドキュメントとimport元(
motion/react)が一致している - 主要な状態変化に「何を分かりやすくする動きか」の目的がある
- Reduced Motionで大きな動きが抑制される
AnimatePresenceの子に安定したkey(indexではない)がある- キーボード操作とフォーカス表示が壊れていない
- スクロール演出が本文の読みやすさを邪魔していない
- 手動確認結果をPRや記事末尾に残している
よくある質問
Q. framer-motion と motion のどっちをインストールすればいい?
新規なら npm install motion、importは motion/react です。framer-motion は古い配布名で、既存プロジェクトに残っているだけ。両方を同じファイルに混ぜると挙動とレビューが混乱するので、どちらかに統一してください。
Q. AnimatePresence を入れたのに exit が効きません。
ほぼ確実に、親要素が {条件 && <子 />} で先に消えています。AnimatePresence は子が消える瞬間を捕まえる仕組みなので、コンテナは残したまま中身だけ増減させてください。次に疑うのは key がindexになっていないか、です。
Q. Next.jsやAstroで使うときの注意は?
motion/react を使うコンポーネントはクライアント側で動く必要があります。Next.jsならファイル先頭に "use client"、Astroなら client:load などのディレクティブでクライアントコンポーネントとして切り出してください。サーバー側で描画しようとすると動きません。
Q. CSSアニメーションと使い分ける基準は? ホバーやボタンの単純な反応はCSSで十分です。Framer Motionが活きるのは、リストの増減・並び替え・要素の出入りなど、Reactの状態変化に動きを連動させたいとき。CSS中心の安全な作り方はClaude CodeでUIアニメーションを安全に実装する実務ガイドにまとめたので、そちらと読み分けてください。
Q. variants を使わず直接 animate を書くのはダメ?
ダメではありません。1要素だけなら initial / animate を直接書く方が読みやすいこともあります。variants の真価は、親で状態を切り替えて子に伝える「まとめて制御」と staggerChildren です。一覧やグループを動かすときに使う、と考えると整理しやすいです。
実際に試した結果
冒頭の「ボヨンボヨン跳ねる管理画面」を直してから、僕のFramer Motionの付き合い方は変わりました。一番効いたのは、テクニックではなく最初のプロンプトにReduced Motionとimport元(motion/react)を明記することでした。後から直すより、初回の差分で制約を入れた方が、レビュー時間も手戻りも目に見えて減ったんです。
AnimatePresence の exit 不発も、原因が「親が先に消える」だと分かってからは一度も踏んでいません。動きの数字を疑う前に、Reactツリーの残り方を見る。これだけで丸一日の溶かしがゼロになりました。Claude CodeとFramer Motionの価値は、派手な動きを一瞬で作ることじゃなくて、状態変化・削除・スクロール・ユーザー設定という見落としやすい条件を、実装とレビューの両方で早めに潰せることだと思っています。
UI部品の土台から整えたいならClaude CodeとRadix UIでアクセシブルなReact UIを作る、デザインシステム側の整理はClaude Codeでshadcn/uiを使いこなす実践ガイドも合わせてどうぞ。Claude Code自体の使い方はAnthropicのClaude Codeドキュメントが一次情報です。プロンプト型を手元に置きたい人はClaudeCodeLabの教材一覧、複数画面をまとめて改善したい人は研修・相談から設計レビュー込みで進めると現実的です。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
Claude Codeに1ファイルだけ直させる指示文のつくり方
「もっと良くして」で40行も変えられた失敗から学んだ、触る範囲・検証・戻し方をセットにしたClaude Code用の依頼文テンプレートを紹介します。
Claude Code の権限拒否から復旧する: 止まった理由を次の安全手順に変える
Claude Code のコマンドが拒否されたとき、焦って許可を広げずに、拒否理由、代替手順、証拠コマンド、再試行条件へ分解する方法。
Claude Codeにビルド→スモークテスト→自動修正を回させる足場の作り方
最小スモークテストの選び方、失敗ログを食わせて直させるループ、回数上限と確認ゲートで暴走を止める方法を、コピペで動くコード付きで紹介します。