关于 mastering-animate-presence
用于审查 Motion 代码中 AnimatePresence 和退出动画实践的 Agent Skill,面向编写或维护动画界面的开发者。它按规则检查代码,并以文件与行号列出问题及汇总。
主要功能
- 01检查 AnimatePresence 包装与 exit 属性
- 02检查列表键和退出动画对称性
- 03检查 Presence Hooks 的使用位置
- 04检查模式选择与布局冲突
- 05检查嵌套退出及交互处理
- 06按文件行号输出问题和统计
适用场景
- 01审查条件渲染的退出动画
- 02检查动态列表的动画设置
- 03定位 usePresence 异步清理问题
- 04评估嵌套动画的时序配置
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ raphaelsalaja/ skill --skill mastering-animate-presence安装后在 Claude Code 中输入/mastering-animate-presence确认已加载。
详细介绍
掌握 AnimatePresence
检查 Motion 代码,了解 AnimatePresence 和退出动画的最佳实践。
工作原理
- 读取指定文件(或提示用户提供文件/模式)
- 根据以下所有规则进行检查
- 以
file:line格式输出检查结果
规则类别
| 优先级 | 类别 | 前缀 |
|---|---|---|
| 1 | 退出动画 | exit- |
| 2 | 存在状态钩子 | presence- |
| 3 | 模式选择 | mode- |
| 4 | 嵌套退出 | nested- |
规则
退出动画规则
exit-requires-wrapper
条件渲染的 motion 元素必须包裹在 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 中的动态列表必须具有唯一的 key。
失败:
<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-hook-in-child
必须在 AnimatePresence 的子组件中调用 useIsPresent,而不是在父组件中调用。
失败:
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
Mode "wait" 几乎会使动画时长翻倍;请相应调整时间设置。
失败:
<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
Mode "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 mode 实现列表重排动画。
失败:
<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 |
参考资料
评论
支持 Markdown