Kemal Yılmaz front-end developer

Exit/Unmount Animations with React

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:

  1. We’re unable to use the usual intuitive isVisible && <ComponentToBeAnimated /> React syntax. We’re passing the isVisible variable to the animation container to control it within the container.
  2. We’re not animating the <ComponentToBeAnimated /> component. We’re animating a dummy div on top of it.
  3. 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, …
  4. The unmounting of the component is heavily dependent on the onAnimationEnd event. 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 &amp;&amp; &lt;motion.div /&gt;</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>&lt;div ref=&#123;nodeRef&#125; /&gt;</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 &amp;&amp; &lt;ComponentToBeAnimatedGSAP /&gt;</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 DefinitionFramer MotionReact Transition GroupGSAP
Only for super-simple casesHigh-level of abstractionNot an animation libraryImperative approach
Prone to growing complexityDedicated <AnimatePresence> component for exit animationsApplies class names during the states of transitionFramework-agnostic, offers a React hook useGSAP
Low production valueReally good for less experiencedYou’re responsible for writing animations with CSSHigh focus on performance, cleanup and keeping the track of animations defined
  1. https://chriscoyier.net/2023/10/30/exit-animations/

  1. Framer Motion / AnimatePrecense
  2. React Transition Group / CSSTransition
  3. GSAP / Exit Animations