mastering-animate-presence by raphaelsalaja/userinterface-wiki
npx skills add https://github.com/raphaelsalaja/userinterface-wiki --skill mastering-animate-presence审查 AnimatePresence 的 Motion 代码以及退出动画的最佳实践。
文件:行号 格式输出检查结果| 优先级 | 类别 | 前缀 |
|---|---|---|
| 1 | 退出动画 | exit- |
| 2 | 存在钩子 | presence- |
| 3 | 模式选择 | mode- |
| 4 |
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
| 嵌套退出 |
nested- |
exit-requires-wrapper条件性 motion 元素必须包裹在 AnimatePresence 中。
失败示例:
{isVisible && (
<motion.div exit={{ opacity: 0 }} />
)}
通过示例:
<AnimatePresence>
{isVisible && (
<motion.div exit={{ opacity: 0 }} />
)}
</AnimatePresence>
exit-prop-requiredAnimatePresence 内部的元素应定义 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-requiredAnimatePresence 内部的动态列表必须具有唯一的 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-childuseIsPresent 必须在 AnimatePresence 的子组件中调用,而不是父组件。
失败示例:
function Parent() {
const isPresent = useIsPresent(); // 错误的位置
return (
<AnimatePresence>
{show && <Child />}
</AnimatePresence>
);
}
通过示例:
function Child() {
const isPresent = useIsPresent(); // 正确的位置
return <motion.div data-exiting={!isPresent} />;
}
presence-safe-to-remove使用 usePresence 时,异步工作完成后务必调用 safeToRemove。
失败示例:
function AsyncComponent() {
const [isPresent, safeToRemove] = usePresence();
useEffect(() => {
if (!isPresent) {
cleanup(); // 从未调用 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>;
// 退出期间按钮仍可点击
}
通过示例:
function Card() {
const isPresent = useIsPresent();
return (
<button onClick={handleClick} disabled={!isPresent}>
Click
</button>
);
}
mode-wait-doubles-duration"wait" 模式几乎会使动画持续时间翻倍;请相应调整时间。
失败示例:
<AnimatePresence mode="wait">
<motion.div transition={{ duration: 0.3 }} />
</AnimatePresence>
// 总时间:~600ms(太慢)
通过示例:
<AnimatePresence mode="wait">
<motion.div transition={{ duration: 0.15 }} />
</AnimatePresence>
// 总时间:~300ms(可接受)
mode-sync-layout-conflict"sync" 模式会导致布局冲突;将退出元素绝对定位。
失败示例:
<AnimatePresence mode="sync">
{items.map(item => (
<motion.div exit={{ opacity: 0 }}>{item}</motion.div>
))}
</AnimatePresence>
// 退出和进入的元素争夺空间
通过示例:
<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>
// 退出期间布局发生偏移
通过示例:
<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>
// 父元素退出时子元素立即消失
通过示例:
<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父元素和子元素的退出持续时间应协调一致。
失败示例:
// 父元素 100ms 退出,子元素 500ms 退出
<motion.div exit={{ opacity: 0 }} transition={{ duration: 0.1 }}>
<motion.div exit={{ scale: 0 }} transition={{ duration: 0.5 }} />
</motion.div>
通过示例:
// 父元素等待子元素或同时退出
<motion.div exit={{ opacity: 0 }} transition={{ duration: 0.2 }}>
<motion.div exit={{ scale: 0 }} transition={{ duration: 0.15 }} />
</motion.div>
审查文件时,按以下格式输出检查结果:
文件:行号 - [规则ID] 问题描述
示例:
components/modal/index.tsx:23 - [exit-requires-wrapper] 条件性 motion.div 未包裹在 AnimatePresence 中
components/modal/index.tsx:45 - [exit-prop-required] motion 元素缺少 exit 属性
检查结果后,输出一个汇总:
| 规则 | 数量 | 严重程度 |
|---|---|---|
exit-requires-wrapper | 2 | 高 |
exit-prop-required | 3 | 高 |
mode-wait-doubles-duration | 1 | 中 |
每周安装量
147
代码仓库
GitHub 星标数
624
首次出现
2026年1月26日
安全审计
安装于
opencode126
codex119
cursor119
gemini-cli113
claude-code109
github-copilot102
Review Motion code for AnimatePresence and exit animation best practices.
file:line format| Priority | Category | Prefix |
|---|---|---|
| 1 | Exit Animations | exit- |
| 2 | Presence Hooks | presence- |
| 3 | Mode Selection | mode- |
| 4 | Nested Exits | nested- |
exit-requires-wrapperConditional motion elements must be wrapped in AnimatePresence.
Fail:
{isVisible && (
<motion.div exit={{ opacity: 0 }} />
)}
Pass:
<AnimatePresence>
{isVisible && (
<motion.div exit={{ opacity: 0 }} />
)}
</AnimatePresence>
exit-prop-requiredElements inside AnimatePresence should have exit prop defined.
Fail:
<AnimatePresence>
{isOpen && (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} />
)}
</AnimatePresence>
Pass:
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
/>
)}
</AnimatePresence>
exit-key-requiredDynamic lists inside AnimatePresence must have unique keys.
Fail:
<AnimatePresence>
{items.map((item, index) => (
<motion.div key={index} exit={{ opacity: 0 }} />
))}
</AnimatePresence>
Pass:
<AnimatePresence>
{items.map((item) => (
<motion.div key={item.id} exit={{ opacity: 0 }} />
))}
</AnimatePresence>
exit-matches-initialExit animation should mirror initial for symmetry.
Fail:
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ scale: 0 }}
/>
Pass:
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }}
/>
presence-hook-in-childuseIsPresent must be called from child of AnimatePresence, not parent.
Fail:
function Parent() {
const isPresent = useIsPresent(); // Wrong location
return (
<AnimatePresence>
{show && <Child />}
</AnimatePresence>
);
}
Pass:
function Child() {
const isPresent = useIsPresent(); // Correct location
return <motion.div data-exiting={!isPresent} />;
}
presence-safe-to-removeWhen using usePresence, always call safeToRemove after async work.
Fail:
function AsyncComponent() {
const [isPresent, safeToRemove] = usePresence();
useEffect(() => {
if (!isPresent) {
cleanup(); // Never calls safeToRemove
}
}, [isPresent]);
}
Pass:
function AsyncComponent() {
const [isPresent, safeToRemove] = usePresence();
useEffect(() => {
if (!isPresent) {
cleanup().then(safeToRemove);
}
}, [isPresent, safeToRemove]);
}
presence-disable-interactionsDisable interactions on exiting elements using isPresent.
Fail:
function Card() {
const isPresent = useIsPresent();
return <button onClick={handleClick}>Click</button>;
// Button clickable during exit
}
Pass:
function Card() {
const isPresent = useIsPresent();
return (
<button onClick={handleClick} disabled={!isPresent}>
Click
</button>
);
}
mode-wait-doubles-durationMode "wait" nearly doubles animation duration; adjust timing accordingly.
Fail:
<AnimatePresence mode="wait">
<motion.div transition={{ duration: 0.3 }} />
</AnimatePresence>
// Total time: ~600ms (too slow)
Pass:
<AnimatePresence mode="wait">
<motion.div transition={{ duration: 0.15 }} />
</AnimatePresence>
// Total time: ~300ms (acceptable)
mode-sync-layout-conflictMode "sync" causes layout conflicts; position exiting elements absolutely.
Fail:
<AnimatePresence mode="sync">
{items.map(item => (
<motion.div exit={{ opacity: 0 }}>{item}</motion.div>
))}
</AnimatePresence>
// Exiting and entering elements compete for space
Pass:
<AnimatePresence mode="popLayout">
{items.map(item => (
<motion.div exit={{ opacity: 0 }}>{item}</motion.div>
))}
</AnimatePresence>
mode-pop-layout-for-listsUse popLayout mode for list reordering animations.
Fail:
<AnimatePresence>
{items.map(item => <ListItem key={item.id} />)}
</AnimatePresence>
// Layout shifts during exit
Pass:
<AnimatePresence mode="popLayout">
{items.map(item => <ListItem key={item.id} />)}
</AnimatePresence>
nested-propagate-requiredNested AnimatePresence must use propagate prop for coordinated exits.
Fail:
<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
Pass:
<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-timingParent and child exit durations should be coordinated.
Fail:
// 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>
Pass:
// 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>
When reviewing files, output findings as:
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
After findings, output a summary:
| Rule | Count | Severity |
|---|---|---|
exit-requires-wrapper | 2 | HIGH |
exit-prop-required | 3 | HIGH |
mode-wait-doubles-duration | 1 | MEDIUM |
Weekly Installs
147
Repository
GitHub Stars
624
First Seen
Jan 26, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode126
codex119
cursor119
gemini-cli113
claude-code109
github-copilot102
UI组件模式实战指南:构建可复用React组件库与设计系统
10,700 周安装