Exit Animations
The lazy exit is the entrance played backwards: same distance, same duration, same curve. It looks tidy, but it gives a goodbye as much time as a hello. Something leaving deserves less ceremony than something arriving.
Dismiss the toasts and watch them go.
Both enter the same way, rising 12px over 240ms. The left one leaves by doing that again in reverse. The right one fades out in half the time, 120ms, with 2px of blur, no travel and an ease-in.
An entrance has a job. Something new is here, and the motion shows you where it came from. An exit has none. The user has already moved on, and anything that makes them watch is in the way. So exits get about half the duration and less movement. They are also the one place ease-in fits, since the element is speeding away and nobody needs to watch it settle.
In React, an element removed from the tree is gone before it can animate.
Motion's AnimatePresence keeps it mounted until its exit animation
finishes.
Exits that keep layout steady
The exit people forget most is a row leaving a list. Without one, the row vanishes and everything below it jumps up in a single frame. Delete a few tasks on each side.
- Call Sarah
- Book flights
- Review PR
- Pay rent
- Call Sarah
- Book flights
- Review PR
- Pay rent
On the right the row collapses its height while it fades, so the rows beneath slide up instead of teleporting. It takes 160ms, short enough that you barely notice it, and long enough that your eye can follow the list.
Usage
import { AnimatePresence, motion } from "motion/react";
<AnimatePresence initial={false}>
{open && (
<motion.div
initial={{ opacity: 0, y: 12, filter: "blur(0px)" }}
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
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;
transition:
opacity 240ms cubic-bezier(0.23, 1, 0.32, 1),
translate 240ms cubic-bezier(0.23, 1, 0.32, 1),
display 240ms allow-discrete;
@starting-style {
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 entrance timing lives on .toast, the exit timing lives on
.toast[hidden], and allow-discrete holds off display: none until the
fade has finished.
Resources
Details that make interfaces feel better - Jakub Krehel's collection of small motion decisions, including why exits should be subtler than entrances.
AnimatePresence - How Motion keeps a removed React element mounted until its exit animation finishes.
@starting-style - The CSS rule that gives an element something to transition from when it first appears.
transition-behavior - How allow-discrete lets an element fade out before display none removes it.