Haliviq
React

key={index} silently breaks Framer Motion exit animations

When you reorder or remove items in a list keyed by index, React matches the wrong old node to the new item, so the exit animation plays on the wrong element. Use a stable id from your data as the key. It costs nothing and also fixes odd input-focus and state bugs in the same list.

What happened

We had a list of cards animated with AnimatePresence, each keyed by its array index. When a user deleted a card from the middle, the last card was the one that played the exit animation, while the deleted card simply changed into its neighbor's content.

The data was correct the whole time. Only the animation looked wrong, and sometimes the focus of inputs inside the cards did too.

Why it happens

React uses the key to decide which old element corresponds to which new one. With index keys, removing item 2 of 5 leaves keys 0 to 3 and drops key 4. React therefore concludes that the last element was removed and updates the props of the remaining ones in place.

AnimatePresence follows the same rule: it plays the exit animation for the child whose key disappeared. So the exit plays on the wrong element. The same mismatch keeps component state, such as a half-typed input value or focus, attached to the position instead of the item. A list that is never reordered or trimmed is fine with index keys.

What we do now

Use a stable, unique id from the data as the key, such as a database id. If items have no id, generate one when the item is created, not during render, because a key from Math.random() inside render changes every time and remounts everything.

With stable keys, the removed item's key is the one that vanishes, so AnimatePresence animates the right element and the neighbors keep their own state.

Key by the id from the datatsx
<AnimatePresence>
  {items.map(item => (
    <motion.li
      key={item.id}
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      exit={{ opacity: 0 }}
    >
      {item.label}
    </motion.li>
  ))}
</AnimatePresence>

Quick checklist

  1. 1Find every list rendered inside AnimatePresence.
  2. 2Replace key={index} with key={item.id}.
  3. 3If there is no id, assign one with crypto.randomUUID() when the item is created.
  4. 4Test by deleting from the middle and by reordering.

Rule of thumb

Key list items by a stable id from your data, never by index, especially when items can be removed or reordered.

We'd love to hear what you're building.

Talk to us