Exit Animations
The lazy exit is the entrance played backwards. Slide up and fade in, then slide down and fade out, same duration, same curve. It is symmetrical, and that is the problem. Something leaving deserves less ceremony than something arriving.
Dismiss the toasts below and watch how each one goes.
Both enter the same way, in 240ms with an ease-out. The left one exits by running that in reverse, so you sit through another 240ms while it slides away. The right one fades out in half the time, with a touch of blur and no travel at all.
The reason is attention. An entrance asks for it: something new is here, and the motion tells you where it came from. An exit is the user saying they are done. Anything that keeps them watching after that point is in the way.
A good rule of thumb: exits run at half the duration of the entrance, and they can use an ease-in, since the thing is accelerating away and nobody needs to see it settle.
Exits that keep layout honest
Removing an item from a list is the exit people forget most. Without one, the item vanishes and everything below it jumps up in a single frame. Delete a few tasks on each side.
- Reply to Sarah
- Book flights to Lisbon
- Review pull request
- Send the invoice
- Reply to Sarah
- Book flights to Lisbon
- Review pull request
- Send the invoice
On the right the row collapses its height while it fades, so the rows beneath slide up to fill the gap instead of teleporting. It is still fast, 160ms, and it never draws attention to itself. It just stops the layout from jumping.
Usage
import { AnimatePresence, motion } from "motion/react";
<AnimatePresence initial={false}>
{open && (
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{
opacity: 0,
filter: "blur(2px)",
transition: { duration: 0.12, ease: "easeIn" },
}}
transition={{ duration: 0.24, ease: [0.23, 1, 0.32, 1] }}
>
Changes saved
</motion.div>
)}
</AnimatePresence>.toast {
display: block;
opacity: 1;
translate: 0 0;
transition:
opacity 240ms var(--ease-out),
translate 240ms var(--ease-out),
display 240ms allow-discrete;
}
@starting-style {
.toast {
opacity: 0;
translate: 0 12px;
}
}
.toast[hidden] {
display: none;
opacity: 0;
filter: blur(2px);
transition:
opacity 120ms ease-in,
filter 120ms ease-in,
display 120ms allow-discrete;
}In CSS, the transition that runs is the one declared on the state you are
moving to. So the enter timing lives on .toast and the exit timing on
.toast[hidden], and allow-discrete is what lets the fade finish before
display: none takes the element out.
Resources
You don't need animations - Emil Kowalski on which animations earn their place, with exits as a recurring example.
Details that make interfaces feel better - Jakub Krehel's collection of small motion decisions, including how things should leave.
AnimatePresence - How to animate a React element out after it has been removed from the tree.
@starting-style - The CSS rule that, together with transition-behavior, lets you animate to and from display none.