Command Palette

Search for a command to run...

Hectal
Shots in this scene
SHOT 15.3Advanced ~23 min · shot 3 of 7

AnimatePresence and Exits

In one line

React removes an element the moment it leaves the tree, so there is nothing left to animate. AnimatePresence keeps the element in the page until its exit animation ends, and its mode decides whether entering and leaving elements overlap.

0/7 · 0%

Think of it like this

A waiter clearing a table. If the plate vanished the instant you stood up, you would never see it go. A good waiter waits until you are done, then takes it away. AnimatePresence is the waiter: React says "remove this", and the plate stays until the exit is finished.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

AnimatePresence
A Motion component that keeps removed children in the page until their exit animation finishes.
exit
The target a motion element animates to when it is removed.
key
React's identity tag for list items. AnimatePresence uses it to know which child left.
mode
sync, wait or popLayout: whether entering and leaving elements overlap, take turns, or leave the layout at once.
presence
Whether a child is still wanted by React. A leaving child is not present but is still in the page.
safeToRemove
A function from usePresence that a component calls to tell AnimatePresence its own exit is done.

Step by step

01See why React cannot do it alone

React compares the old list with the new one and deletes the DOM node of any item that is gone, in the same commit. Nothing is left for an exit to move.

See why React cannot do it alonediagram
Rendering diagram…

02Remove an item

Run the first example. At 300 ms the page removes Lights by itself. Pause the Timeline during the fade: two li elements are in the page, one at half opacity. The console says exit complete, 2 in the DOM just before the node goes.

03Switch the mode

Choose popLayout, press Add, and remove an item: the leaving row is position: absolute while it fades, so the rest close up at once. With sync it keeps its place until it is gone.

04Take the key away

In the second example the page with no key swaps text instantly. Only the keyed copy fades and logs exit complete: keyed.

Try it yourself

  1. 1

    Compare the three modes

    In the first example, change the mode to wait, popLayout and sync, and add an item right after removing one. Measured 100 ms after the page removes Lights, with a new item added 70 ms before that reading: with sync and popLayout the leaving row and the new row are both in the page (popLayout has the leaving row at position: absolute); with wait only the leaving row exists, and the new one waits for it. wait is meant for one child at a time, so with a list it also prints the "odd visual behaviour" warning and Lock is not on screen either.

  2. 2

    Use the index as the key

    Copy the keyed list from the data (Lock, Lights, Bell) and use key={index}. Remove Lock. Measured 150 ms later: the page shows Lights, Bell and a fading Bell. The row that left the data is not the one that fades.

Code & diagrams

A list with exits and a mode switch

Add, remove with x, and change the mode. At 300 ms the page removes Item 2 for you. The console reports how many `li` were in the page when the exit finished.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.

Expected console output

exit complete, 2 in the DOM
No key: no exit

Both rows swap Specs for Gallery. Only the keyed one fades out and in, one after the other (`wait`); the other changes at once.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.

Expected console output

exit complete: keyed
Put it back mid-exit

The row is removed, then returned 100 ms later while still fading. It comes back and its exit never completes.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.

Expected console output

Lock isPresent=true
Lock isPresent=false
Lock isPresent=true
in the DOM: 1

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Remove the key from a swapped child

In the second example's keyed branch, delete key={i}.

terminal
$ Press Next page and read the console
── what you'll see ──
Nothing is logged: `exit complete` never fires. The text changes to Gallery at once with no fade.

Myth vs fact

Myth

Wrap any component in AnimatePresence and its inner motion elements will exit.

Fact

The element that is removed must be a direct child of AnimatePresence with a stable key. Elements inside it can then run their own exit.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    The leaving child is rendered from a saved copy of the old element, so it shows the *old* props. Do not expect it to react to state that changed after it started leaving.

  • ▸

    Exit animations delay DOM removal. A long exit on a modal keeps its focus trap and scroll lock alive for as long: set the lock off on onExitComplete, or make the exit short.

  • ▸

    mode="popLayout" measures the leaving element and pins it; a parent without position: relative makes it jump relative to the wrong box. The leaving row also stops taking part in flex or grid flow immediately, so its siblings animate with layout (Topic 15.4).

Remember this

  1. 1

    The problem this solves: {show && <Panel />} makes the panel disappear in one frame, because React deletes its DOM node in the same commit. CSS cannot animate a node that no longer exists. Without help, enter animations are easy and exit animations are impossible.

  2. 2

    How it works. AnimatePresence remembers its children from the previous render. When a child with a key is no longer returned, it keeps rendering it, tells it (through context) that it is no longer present, and the child's exit prop animates. When every exit animation of that child has finished, AnimatePresence drops it and calls onExitComplete. The callback runs just before the removal: in the first example it reports 2 in the DOM when one of two items has finished leaving (measured).

  3. 3

    Keys decide everything. AnimatePresence tells children apart by key. A list needs a stable id as key. With no key, switching content from one page to another is the same element to React, so nothing leaves and nothing enters: in the second example the unkeyed text changes to Gallery at once while the keyed one fades out first and calls onExitComplete. With index keys, removing the first of three items makes the *last* item play the exit, and the other rows change text (measured: Bell faded while Lock was removed from the data).

  4. 4

    Modes. sync (the default) lets entering and leaving elements run together, so the layout can briefly hold both. wait lets one element leave, and only then does the next enter; it is built for one child at a time, and with several children Motion logs You're attempting to animate multiple children within AnimatePresence, but its mode is set to "wait". This will lead to odd visual behaviour. (measured). popLayout takes the leaving element out of the layout at once (measured: its computed position was absolute while it faded), so its siblings move into place immediately; the parent must be positioned.

  5. 5

    Rapid changes. Put an element back while it is still leaving and it returns: usePresence() reports isPresent: false and then true again, the component is not re-created, and its exit never completes (measured in the third example: no "exit done", and one li in the page).

  6. 6

    Presence hooks. useIsPresent() tells a component it is leaving, for example to disable its buttons. usePresence() returns [isPresent, safeToRemove]; a component that drives its own exit (a GSAP timeline, a canvas fade) calls safeToRemove() when done. initial={false} on AnimatePresence skips the enter animation of children present on the first render, which the configurator uses.

  7. 7

    Accessibility. A leaving element is still in the page while it exits, so keep exits short (about 150 to 250 ms), never block focus or input on them, and remember that under reduced motion Motion makes position and scale changes instant but still animates opacity (Topic 15.6): an exit={{ opacity: 0, x: 40 }} becomes a plain fade.

Explain it without notes

01

Why can CSS alone not animate an element that React removes, and what does AnimatePresence change?

02

What happens when you remove the key, or use the array index as the key?

Practice

01

Make a toast that slides up 16 px and fades in, and fades out when dismissed. Show it for one toast at a time.

02

A cart row must not be clickable while it is leaving. Write the row.

Done when you can

  • You can explain why an exit needs AnimatePresence

  • You can choose sync, wait or popLayout for a case and say what it changes

  • You can say what a missing key or an index key does to exits

  • You can handle an element that returns while it is leaving