mcpField
M
mastering-animate-presence

mastering-animate-presence

github.com/raphaelsalajaDesign & UI42325 installsUpdated today
Repository

About mastering-animate-presence

An Agent Skill for reviewing Motion code against AnimatePresence and exit-animation practices. It is aimed at developers maintaining animated interfaces, and reports rule-based findings by file and line with a summary table.

Key features

  1. 01Check AnimatePresence wrappers and exit props
  2. 02Review list keys and animation symmetry
  3. 03Check placement of Presence Hooks
  4. 04Review mode selection and layout conflicts
  5. 05Check nested exits and interaction handling
  6. 06Report findings by file, line, and count

Use cases

  1. 01Review exits for conditionally rendered elements
  2. 02Check animation setup for dynamic lists
  3. 03Find async cleanup issues with usePresence
  4. 04Assess timing for nested animations

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/raphaelsalaja/skill --skill mastering-animate-presence

After installing, type/mastering-animate-presencein Claude Code to confirm it loaded.

Details

From the project README

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

PriorityCategoryPrefix
1Exit Animationsexit-
2Presence Hookspresence-
3Mode Selectionmode-
4Nested Exitsnested-

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 space

Pass:

<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 exit

Pass:

<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 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-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 element

Summary Table

After findings, output a summary:

RuleCountSeverity
exit-requires-wrapper2HIGH
exit-prop-required3HIGH
mode-wait-doubles-duration1MEDIUM

References

Comments

Markdown supported