Exit/Unmount Animations with React
- Exit Animations
- Unmount Animations
- React
- Framer Motion
- React Transition Group
- GSAP
Exit animations are not a React-specific problem. React only makes the problem worse, much like it does with everything else. Here, I'll simply point out the problem and provide the bare minimum solution for a plain React installation. Then, I'll try to implement similar solutions in a couple of different motion libraries.
Let’s jump into my problem right away. I want to animate a React component while it is unmounting and I don’t want it to be just invisible. I want it to be dropped from the DOM tree as well. (You’ve got the idea!)
In short, one way or another, you’ll need an animation library for a production-grade solution. (I’ll return back to this.) Here the goal is just to point out the problem.
To control the visibility of a component in React, we use statements such as:
isVisible && <ComponentToBeAnimated />
When you set false for the isVisible variable, the component and its business logic will be gone. This is why we can’t use the component’s body to control the unmounting logic. What we need is a simple container.
The key to the solution is to wrap the animated component in a container. In this configuration, we’ll not have the rendered DOM elements of the <ComponentToBeAnimated /> in the DOM tree (or React tree) but we’ll keep the <AnimationContainer> in the React component tree as well as keeping the unmounting logic intact within the container.
<AnimationContainer isVisible={isVisible}>
<ComponentToBeAnimated />
</AnimationContainer>
Here’s the quick solution:
/*
<button onClick={
() => {
setIsVisible(prev => !prev)
}
}
>Toggle
</button>
*/
function AnimationContainer({ isVisible, children }) {
const [isVisibleInner, setIsVisibleInner] = useState(false)
// controls enter/mounting state only
useEffect(() => {
if (isVisible) {
setIsVisibleInner(true)
}
}, [isVisible])
return (
<div className="outer">
{isVisibleInner && (
<div
className={`dummy-div-to-avoid-prop-drilling ${isVisible ? 'animate-in' : 'animate-out'}`}
onAnimationEnd={() => {
// controls exit/unmount only
if (isVisible === false) {
setIsVisibleInner(false)
}
}}
>
{/* The children here is the `<ComponentToBeAnimated />` component! */}
{children}
</div>
)}
</div>
)
}
Okay, that summarizes the problem definition nicely. Here are the bad parts:
- We’re unable to use the usual intuitive
isVisible && <ComponentToBeAnimated />React syntax. We’re passing theisVisiblevariable to the animation container to control it within the container. - We’re not animating the
<ComponentToBeAnimated />component. We’re animating a dummydivon top of it. - The approach is prone to growing complexity when it comes to propagate the outer visibility controller to the children to get rid of the dummy
<div>, or to develop multiple animated children, animation orchestration, cancelling pending animations, … - The unmounting of the component is heavily dependent on the
onAnimationEndevent. No fallback mechanism.
See the demo on GitHub (Problem Definition)
Solutions with Motion Libraries
Any animation framework would do the job. See the examples and demos below. Head to the demo pages for the details and short explanations about each implementation.
Why do we need a motion framework? Because real-world problems are never that easy to fix. And animations are not free from budget concerns. Maintaining them will both consume your performance budget as well as your development time. The solution offered above is just to reveal the problem and give an idea about it.
Framer Motion
This is the only framework that offers a built-in solution for exit animations amongst others mentioned here.
One notable thing I liked about framer-motion library, is that I think they know the culture behind those “animation-lover” businesses and the technical knowledge level of them. The abstraction level and <motion.*> components are really good building blocks on that aspect.
/*
<button onClick={
() => {
setIsVisibleFramerMotion(prev => !prev)
}
}
>Toggle
</button>
*/
<AnimatePresence>
{isVisibleFramerMotion && (
<motion.div
className="inner"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.5 }}
>
<code>isVisibleFramerMotion && <motion.div /></code>
</motion.div>
)}
</AnimatePresence>
See the demo on GitHub (Framer Motion)
React Transition Group
A part of React community, released to simplify transitions, especially entering and exiting transitions with React framework. A good choice if you’re seeking for a thin layer to find out mounting/unmounting states of a React component.
/*
<button onClick={
() => {
setIsVisibleReactTransitionGroup(prev => !prev)
}
}
>Toggle
</button>
*/
<CSSTransition
nodeRef={nodeRef}
timeout={5000}
in={isVisibleReactTransitionGroup}
appear={isVisibleReactTransitionGroup}
unmountOnExit
addEndListener={(done) => {
nodeRef.current.addEventListener('animationend', done, false)
}}
>
<div className="inner" ref={nodeRef}>
<code><div ref={nodeRef} /></code>
</div>
</CSSTransition>
/* react-transition-group demo animations in CSS */
/*
.enter,
.appear {
opacity: 0;
}
.enter-active,
.appear-active {
animation: fade-in 500ms forwards;
}
.exit {
opacity: 1;
}
.exit-active {
animation: fade-out 500ms forwards;
}
*/
See the demo on GitHub (React Transition Group)
GSAP
A lower-level abstraction with an imperative approach to the animations. It has a steep learning curve however, it offers a framework-agnostic animation experience at the end.
Exit handler & animation remain within the parent:
/*
<button onClick={
() => {
isVisibleGSAP ? exitHandler() : setIsVisibleGSAP(true)
}
}
>
Toggle
</button>
*/
const exitHandler = contextSafe(() => {
gsap.to('.inner.gsap', {
opacity: 0,
duration: 0.5,
onComplete: () => {
setIsVisibleGSAP(false)
},
})
})
{isVisibleGSAP && <ComponentToBeAnimatedGSAP />}
Enter animation is contained within the component itself:
function ComponentToBeAnimatedGSAP() {
useGSAP(() => {
gsap.from('.inner.gsap', {
opacity: 0,
duration: 0.5,
})
})
return (
<div className="inner gsap">
<code>isVisibleGSAP && <ComponentToBeAnimatedGSAP /></code>
</div>
)
}
See the demo on GitHub (GSAP)
Conclusion / My Take
If your animations don’t add any value to the user experience, just skip using them. 😄 I don’t think anyone needs an animation where “a whole page moves 400px left then, bounces 2 times, then comes back 400px while setting its opacity from 0.5 to 1” … before reading an article about expected inflation rates in Eurozone.
Don’t overuse animations. If you don’t need it, don’t use it at all!
Comparison
⚠️ Comparisons are made only for the enter/exit animations context.
| Problem Definition | Framer Motion | React Transition Group | GSAP |
|---|---|---|---|
| Only for super-simple cases | High-level of abstraction | Not an animation library | Imperative approach |
| Prone to growing complexity | Dedicated <AnimatePresence> component for exit animations | Applies class names during the states of transition | Framework-agnostic, offers a React hook useGSAP |
| Low production value | Really good for less experienced | You’re responsible for writing animations with CSS | High focus on performance, cleanup and keeping the track of animations defined |