mastering-animate-presence について
Motion コードの AnimatePresence と退出アニメーションの実践を確認するエージェントスキルです。アニメーション UI を保守する開発者向けに、ルール違反をファイル名と行番号で示し、集計します。
主な機能
- 01AnimatePresence のラップと exit プロパティの確認
- 02リストキーとアニメーション対称性の確認
- 03Presence Hooks の呼び出し位置の確認
- 04モード選択とレイアウト競合の確認
- 05ネストした退出と操作制御の確認
- 06ファイル・行番号付きの指摘と集計
活用シーン
- 01条件付き要素の退出アニメーションのレビュー
- 02動的リストのアニメーション設定の確認
- 03usePresence の非同期クリーンアップ問題の特定
- 04ネストしたアニメーションのタイミング評価
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ raphaelsalaja/ skill --skill mastering-animate-presenceインストール後、Claude Code で/mastering-animate-presenceを入力して読み込まれたか確認します。
詳細
AnimatePresence を使いこなす
AnimatePresence と終了アニメーションのベストプラクティスに沿って、Motion のコードをレビューします。
仕組み
- 指定されたファイルを読み込みます(または、ファイルやパターンをユーザーに確認します)
- 以下のすべてのルールに照らして確認します
file:line形式で指摘事項を出力します
ルールのカテゴリ
| 優先度 | カテゴリ | プレフィックス |
|---|---|---|
| 1 | 終了アニメーション | exit- |
| 2 | Presence フック | presence- |
| 3 | モードの選択 | mode- |
| 4 | ネストした終了アニメーション | nested- |
ルール
終了アニメーションのルール
exit-requires-wrapper
条件付きのモーション要素は AnimatePresence でラップする必要があります。
失敗例:
{isVisible && (
<motion.div exit={{ opacity: 0 }} />
)}
成功例:
<AnimatePresence>
{isVisible && (
<motion.div exit={{ opacity: 0 }} />
)}
</AnimatePresence>
exit-prop-required
AnimatePresence 内の要素には exit プロパティを定義する必要があります。
失敗例:
<AnimatePresence>
{isOpen && (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} />
)}
</AnimatePresence>
成功例:
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
/>
)}
</AnimatePresence>
exit-key-required
AnimatePresence 内の動的リストには一意のキーが必要です。
失敗例:
<AnimatePresence>
{items.map((item, index) => (
<motion.div key={index} exit={{ opacity: 0 }} />
))}
</AnimatePresence>
成功例:
<AnimatePresence>
{items.map((item) => (
<motion.div key={item.id} exit={{ opacity: 0 }} />
))}
</AnimatePresence>
exit-matches-initial
対称性を保つため、終了アニメーションは初期状態を反映する必要があります。
失敗例:
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ scale: 0 }}
/>
成功例:
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }}
/>
Presence フックのルール
presence-hook-in-child
useIsPresent は AnimatePresence の親ではなく、子から呼び出す必要があります。
失敗例:
function Parent() {
const isPresent = useIsPresent(); // Wrong location
return (
<AnimatePresence>
{show && <Child />}
</AnimatePresence>
);
}
成功例:
function Child() {
const isPresent = useIsPresent(); // Correct location
return <motion.div data-exiting={!isPresent} />;
}
presence-safe-to-remove
usePresence を使う場合、非同期処理の後に必ず safeToRemove を呼び出してください。
失敗例:
function AsyncComponent() {
const [isPresent, safeToRemove] = usePresence();
useEffect(() => {
if (!isPresent) {
cleanup(); // Never calls safeToRemove
}
}, [isPresent]);
}
成功例:
function AsyncComponent() {
const [isPresent, safeToRemove] = usePresence();
useEffect(() => {
if (!isPresent) {
cleanup().then(safeToRemove);
}
}, [isPresent, safeToRemove]);
}
presence-disable-interactions
isPresent を使って、終了中の要素のインタラクションを無効にしてください。
失敗例:
function Card() {
const isPresent = useIsPresent();
return <button onClick={handleClick}>Click</button>;
// Button clickable during exit
}
成功例:
function Card() {
const isPresent = useIsPresent();
return (
<button onClick={handleClick} disabled={!isPresent}>
Click
</button>
);
}
モード選択のルール
mode-wait-doubles-duration
モード「wait」ではアニメーション時間がほぼ 2 倍になるため、それに合わせてタイミングを調整してください。
失敗例:
<AnimatePresence mode="wait">
<motion.div transition={{ duration: 0.3 }} />
</AnimatePresence>
// Total time: ~600ms (too slow)
成功例:
<AnimatePresence mode="wait">
<motion.div transition={{ duration: 0.15 }} />
</AnimatePresence>
// Total time: ~300ms (acceptable)
mode-sync-layout-conflict
モード「sync」ではレイアウトの競合が発生するため、終了する要素を絶対配置してください。
失敗例:
<AnimatePresence mode="sync">
{items.map(item => (
<motion.div exit={{ opacity: 0 }}>{item}</motion.div>
))}
</AnimatePresence>
// Exiting and entering elements compete for space
成功例:
<AnimatePresence mode="popLayout">
{items.map(item => (
<motion.div exit={{ opacity: 0 }}>{item}</motion.div>
))}
</AnimatePresence>
mode-pop-layout-for-lists
リストの並べ替えアニメーションには popLayout モードを使ってください。
失敗例:
<AnimatePresence>
{items.map(item => <ListItem key={item.id} />)}
</AnimatePresence>
// Layout shifts during exit
成功例:
<AnimatePresence mode="popLayout">
{items.map(item => <ListItem key={item.id} />)}
</AnimatePresence>
ネストした終了アニメーションのルール
nested-propagate-required
終了アニメーションを連携させるには、ネストした AnimatePresence で propagate プロパティを使う必要があります。
失敗例:
<AnimatePresence>
{isOpen && (
<motion.div exit={{ opacity: 0 }}>
<AnimatePresence>
{items.map(item => (
<motion.div key={item.id} exit={{ scale: 0 }} />
))}
</AnimatePresence>
</motion.div>
)}
</AnimatePresence>
// Children vanish instantly when parent exits
成功例:
<AnimatePresence propagate>
{isOpen && (
<motion.div exit={{ opacity: 0 }}>
<AnimatePresence propagate>
{items.map(item => (
<motion.div key={item.id} exit={{ scale: 0 }} />
))}
</AnimatePresence>
</motion.div>
)}
</AnimatePresence>
nested-consistent-timing
親と子の終了時間を調整してください。
失敗例:
// Parent exits in 100ms, children in 500ms
<motion.div exit={{ opacity: 0 }} transition={{ duration: 0.1 }}>
<motion.div exit={{ scale: 0 }} transition={{ duration: 0.5 }} />
</motion.div>
成功例:
// Parent waits for children or exits simultaneously
<motion.div exit={{ opacity: 0 }} transition={{ duration: 0.2 }}>
<motion.div exit={{ scale: 0 }} transition={{ duration: 0.15 }} />
</motion.div>
出力形式
ファイルをレビューする際は、次の形式で指摘事項を出力します。
file:line - [rule-id] description of issue
Example:
components/modal/index.tsx:23 - [exit-requires-wrapper] Conditional motion.div not wrapped in AnimatePresence
components/modal/index.tsx:45 - [exit-prop-required] Missing exit prop on motion element
まとめ表
指摘事項の後に、次のまとめを出力します。
| ルール | 件数 | 重大度 |
|---|---|---|
exit-requires-wrapper | 2 | HIGH |
exit-prop-required | 3 | HIGH |
mode-wait-doubles-duration | 1 | MEDIUM |