Skip to content
Design
Skill

/mastering-animate-presence

Audit Motion/Framer Motion code for AnimatePresence best practices. Use when reviewing exit animations, modals, or presence state. Outputs file:line findings.

BOOST
From plugin
raphaelsalaja-skill
417 skills
Install
$ npx -y skills add raphaelsalaja/skill --skill mastering-animate-presence --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/mastering-animate-presence

Context preview

The summary Claude sees to decide when to auto-load this skill.

Audit Motion/Framer Motion code for AnimatePresence best practices. Use when reviewing exit animations, modals, or presence state. Outputs file:line findings.

SKILL.md

mastering-animate-presence.SKILL.md
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:**

{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 }}>
  <mot
Read more
Ships withraphaelsalaja-skill

A skill file based on the articles written on userinterface.wiki. Designed for designers and engineers to help them build better user interfaces.

Get the whole plugin

Other skills on raphaelsalaja-skill.