Skip to main content
skill CX Codex

mastering-animate-presence — Claude Code skill: audit exit animations

Checks Motion and Framer Motion code for AnimatePresence mistakes and exit animation problems. Useful when reviewing modals and lists.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add raphaelsalaja/skill --skill mastering-animate-presence -a claude-code
Open on GitHub

What mastering-animate-presence does

The skill reads the files you point it at and checks them against a set of AnimatePresence rules. It flags conditional motion elements with no wrapper, a missing exit prop, index keys instead of item.id, exit animations that do not mirror initial. Every finding comes out as file:line with a rule id, like components/modal/index.tsx:23 - [exit-requires-wrapper]. A summary table with counts and severity follows the list.

Rules sit in four groups: exit animations, presence hooks, mode selection, nested exits. The presence hooks get their own treatment — where useIsPresent belongs, and why safeToRemove has to run after async work. Mode rules are blunt about tradeoffs: wait nearly doubles animation duration, sync fights with layout, and list reordering wants popLayout. Nested AnimatePresence needs the propagate prop, otherwise children vanish the moment the parent exits.

The audit runs on the files you name, or the skill asks for a path and pattern. It changes nothing and runs nothing — you get a report. No Motion or Framer Motion in the project means there is nothing here to catch.

What it helps with

  • catch conditional motion elements missing an AnimatePresence wrapper
  • verify that elements inside AnimatePresence define an exit prop
  • swap index keys for stable item.id keys in animated lists
  • coordinate exit durations between a parent and its children
  • pick the right AnimatePresence mode for list reordering

Example prompts for the agent

  • audit components/modal/index.tsx against the AnimatePresence rules
  • run an exit animation audit over every file in src/components
  • check src/pages/dashboard.tsx — the modal disappears with no animation

How to install mastering-animate-presence

1Skills CLI (Vercel)

npx skills add raphaelsalaja/skill --skill mastering-animate-presence -a claude-code

2Manually: put the files in place

~/.claude/skills/mastering-animate-presence/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/mastering-animate-presence/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about mastering-animate-presence

How do I install mastering-animate-presence in Claude?

Run npx skills add raphaelsalaja/skill --skill mastering-animate-presence -a claude-code. The skill lands in ~/.claude/skills/mastering-animate-presence/SKILL.md.

What does mastering-animate-presence do?

It checks Motion and Framer Motion code against AnimatePresence rules and reports findings as file:line plus a summary table by rule.

Do I need Motion installed in the project?

The skill audits AnimatePresence code, so a project without Motion or Framer Motion gives it nothing to check.

Source text

SKILL.md · MIT
---
name: mastering-animate-presence
description: Audit Motion/Framer Motion code for AnimatePresence best practices. Use when reviewing exit animations, modals, or presence state. Outputs file:line findings.
license: MIT
metadata:
  author: raphael-salaja
  version: "2.0.0"
  source: /content/mastering-animate-presence/index.mdx
---

# 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:**
```tsx
{isVisible && (
  <motion.div exit={{ opacity: 0 }} />
)}
```

**Pass:**
```tsx
<AnimatePresence>
  {isVisible && (
    <motion.div exit={{ opacity: 0 }} />
  )}
</AnimatePresence>
```

#### `exit-prop-required`
Elements inside AnimatePresence should have exit prop defined.

**Fail:**
```tsx
<AnimatePresence>
  {isOpen && (
    <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} />
  )}
</AnimatePresence>
```

**Pass:**
```tsx
<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:**
```tsx
<AnimatePresence>
  {items.map((item, index) => (
    <motion.div key={index} exit={{ opacity: 0 }} />
  ))}
</AnimatePresence>
```

**Pass:**
```tsx
<AnimatePresence>
  {items.map((item) => (
    <motion.div key={item.id} exit={{ opacity: 0 }} />
  ))}
</AnimatePresence>
```

#### `exit-matches-initial`
Exit animation should mirror initial for symmetry.

**Fail:**
```tsx
<motion.div
  initial={{ opacity: 0, y: 20 }}
  animate={{ opacity: 1, y: 0 }}
  exit={{ scale: 0 }}
/>
```

**Pass:**
```tsx
<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:**
```tsx
function Parent() {
  const isPresent = useIsPresent(); // Wrong location
  return (
    <AnimatePresence>
      {show && <Child />}
    </AnimatePresence>
  );
}
```

**Pass:**
```tsx
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:**
```tsx
function AsyncComponent() {
  const [isPresent, safeToRemove] = usePresence();

  useEffect(() => {
    if (!isPresent) {
      cleanup(); // Never calls safeToRemove
    }
  }, [isPresent]);
}
```

**Pass:**
```tsx
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:**
```tsx
function Card() {
  const isPresent = useIsPresent();
  return <button onClick={handleClick}>Click</button>;
  // Button clickable during exit
}
```

**Pass:**
```tsx
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:**
```tsx
<AnimatePresence mode="wait">
  <motion.div transition={{ duration: 0.3 }} />
</AnimatePresence>
// Total time: ~600ms (too slow)
```

**Pass:**
```tsx
<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:**
```tsx
<AnimatePresence mode="sync">
  {items.map(item => (
    <motion.div exit={{ opacity: 0 }}>{item}</motion.div>
  ))}
</AnimatePresence>
// Exiting and entering elements compete for space
```

**Pass:**
```tsx
<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:**
```tsx
<AnimatePresence>
  {items.map(item => <ListItem key={item.id} />)}
</AnimatePresence>
// Layout shifts during exit
```

**Pass:**
```tsx
<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:**
```tsx
<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:**
```tsx
<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:**
```tsx
// 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:**
```tsx
// 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:

| Rule | Count | Severity |
|------|-------|----------|
| `exit-requires-wrapper` | 2 | HIGH |
| `exit-prop-required` | 3 | HIGH |
| `mode-wait-doubles-duration` | 1 | MEDIUM |

## References

- [Motion AnimatePresence Documentation](https://motion.dev/docs/react-animate-presence)
- [MDN @starting-style](https://developer.mozilla.org/en-US/docs/Web/CSS/@starting-style)

© raphael-salaja · MIT · raphaelsalaja/skill

Similar skills

Everything for Frontend and UI →

From the same set2

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor