
Mastering Animate Presence
- 203 installs
- 32 repo stars
- Updated March 17, 2026
- raphaelsalaja/skill
Audit Motion and Framer Motion usage so modals, conditional UI, and exit animations follow AnimatePresence rules with file:line findings.
About
mastering-animate-presence is a focused reviewer skill for solo builders shipping polished React interfaces with Motion or Framer Motion. The agent reads specified files or patterns, then checks conditional motion trees against four rule families prefixed exit-, presence-, mode-, and nested-. Common failures—mounting motion.div without AnimatePresence, missing exit props, wrong presence modes, or nested exit timing—are illustrated with fail and pass snippets so fixes are obvious. Output is deliberately mechanical: file:line findings you can triage like a linter. Invoke it when reviewing PRs that touch dialogs, drawers, route transitions, or any UI that unmounts with animation. It does not replace visual QA in the browser, but it prevents the structural mistakes that make exits snap or flash. Pair with your component tests and a quick manual modal pass before launch.
- 4 prioritized rule categories: Exit Animations, Presence Hooks, Mode Selection, Nested Exits
- Fail/pass examples for exit-requires-wrapper and exit-prop-required patterns
- Outputs findings in file:line format for direct navigation in the editor
- Targets conditional motion elements, modals, and presence-driven UI
- MIT-licensed checker aligned with mastering-animate-presence MDX source v2.0.0
Mastering Animate Presence by the numbers
- 203 all-time installs (skills.sh)
- +10 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #339 of 1,352 Code Review & Quality skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/raphaelsalaja/skill --skill mastering-animate-presenceAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 203 |
|---|---|
| repo stars | ★ 32 |
| Security audit | 3 / 3 scanners passed |
| Last updated | March 17, 2026 |
| Repository | raphaelsalaja/skill ↗ |
What it does
Audit Motion and Framer Motion usage so modals, conditional UI, and exit animations follow AnimatePresence rules with file:line findings.
Files
Mastering AnimatePresence
Review Motion code for AnimatePresence and exit animation best practices.
How It Works
1. Read the specified files (or prompt user for files/pattern) 2. Check against all rules below 3. Output findings in file:line format
Rule Categories
| Priority | Category | Prefix |
|---|---|---|
| 1 | Exit Animations | exit- |
| 2 | Presence Hooks | presence- |
| 3 | Mode Selection | mode- |
| 4 | Nested Exits | nested- |
Rules
Exit Animation Rules
exit-requires-wrapper
Conditional 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-required
Elements 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-required
Dynamic 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-initial
Exit 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 Rules
presence-hook-in-child
useIsPresent 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-remove
When 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-interactions
Disable 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 Selection Rules
mode-wait-doubles-duration
Mode "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-conflict
Mode "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 spacePass:
<AnimatePresence mode="popLayout">
{items.map(item => (
<motion.div exit={{ opacity: 0 }}>{item}</motion.div>
))}
</AnimatePresence>mode-pop-layout-for-lists
Use popLayout mode for list reordering animations.
Fail:
<AnimatePresence>
{items.map(item => <ListItem key={item.id} />)}
</AnimatePresence>
// Layout shifts during exitPass:
<AnimatePresence mode="popLayout">
{items.map(item => <ListItem key={item.id} />)}
</AnimatePresence>Nested Exit Rules
nested-propagate-required
Nested 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 exitsPass:
<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 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>Output Format
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 elementSummary Table
After findings, output a summary:
| Rule | Count | Severity |
|---|---|---|
exit-requires-wrapper | 2 | HIGH |
exit-prop-required | 3 | HIGH |
mode-wait-doubles-duration | 1 | MEDIUM |
References
Related skills
FAQ
Is Mastering Animate Presence safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.