Email client
Email client

Watch everything for $29/month.

Join Build UI Pro

Email client

Add animation to unmounting elements using the versatile AnimatePresence component.

Video resources

Summary

This email inbox is a great candidate for animation, since the message list is disorienting when it updates. To animate receiving a new message, let's add a mount animation to each list item.

We'll turn each li into a motion element, and then add initial and animate props to trigger a mount animation:

  {[...messages].reverse().map((id) => (
-   <li key={id}>
+   <motion.li initial={{ opacity: 0 }} animate={{ opacity: 1 }} key={id}>
      {/*  */}
-   </li>
+   </motion.li>
  ))}

New messages now fade in, which helps our user see which messages are being added to the list. But existing messages still jump to their new positions. It'd be nice if we could smoothly animate them instead.

Animating height

Ideally if we animated the height of new messages from 0 to auto, the existing messages should just smoothly shift into place. Unfortunately, CSS has never been able to animate height from auto to any other value, so there's never really been a good answer for this.

Until now! Framer Motion adds exactly this capability for us, in the most intuitive way possible:

<motion.li
  initial={{ opacity: 0, height: 0 }}
  animate={{ opacity: 1, height: "auto" }}
  key={id}
>
  {/*  */}
</motion.li>

Just by setting height: 0 as our initial state and height: 'auto' as our animate state, Framer Motion animates the height of mounting elements, which smoothly shifts the existing messages down. Nice!

Now if we look closely at our entering messages, we'll see a small jump as the animation completes, which looks like a bug. If you start using height: "auto" more in your own animations, you'll see this behavior from time to time, and it's because the browser has no 100% reliable way of calculating an element's height.

Typically removing any margin or padding from the motion element will be enough to eliminate the undesirable jump. In our case, our motion element has some vertical padding on it, which we can just move to an inner div:

  <motion.li
    initial={{ opacity: 0, height: 0 }}
    animate={{ opacity: 1, height: "auto" }}
    key={id}
-   className="py-2"
  >
+   <div className="py-2">
      {/*  */}
+   </div>
  </motion.li>

And now, our animation is buttery smooth!

Unmount animations

When we click a message, it's a bit jarring since it immediately disappears from the list. We can add animations to exiting messages by using the exit prop.

For the exit prop to have any effect on a motion element, that element needs to be wrapped in the <AnimatePresence /> component from Framer Motion.

<AnimatePresence>
  {[...messages].reverse().map((id) => (
    <motion.li
      initial={{ opacity: 0, height: 0 }}
      animate={{ opacity: 1, height: "auto" }}
      exit={{ opacity: 0, height: 0 }}
      key={id}
    >
      {/*  */}
    </motion.li>
  ))}
</AnimatePresence>

Now when we archive a message, it fades out while animating its height to 0, which also smoothly shifts the existing messages up to their new positions!

<AnimatePresence /> is one of the most useful components in Framer Motion, and you'll likely find yourself using it all over your apps. Just make sure that it's always mounted, even as you conditionally mount and unmount child components.

For example, this won't work as you expect:

<div>
  <button onClick={toggleShowMore}>Show more</button>

  {isShowingMore && (
    {/* ๐Ÿ”ด AnimatePresence is unmounted, so no exit animations run */}
    <AnimatePresence>
      <motion.p
        initial={{ opacity: 0 }}
        animate={{ opacity: 1 }}
        exit={{ opacity: 0 }}
      >
        {/*  */}
      </motion.p>
    </AnimatePresence>
  )}
</div>

Since the <AnimatePresence /> component is being unmounted, the exit animation will never trigger on the motion.p element.

Instead, move the <AnimatePresence /> outside the conditional, so that it stays mounted while its child is being unmounted:

<div>
  <button onClick={toggleShowMore}>Show more</button>

  {/* ๐ŸŸข AnimatePresence stays mounted, so exit animations run */}
  <AnimatePresence>
    {isShowingMore && (
      <motion.p
        initial={{ opacity: 0 }}
        animate={{ opacity: 1 }}
        exit={{ opacity: 0 }}
      >
        {/*  */}
      </motion.p>
    )}
  </AnimatePresence>
</div>

This is something that's easy to mess up when you first start using AnimatePresence, but quickly becomes second nature.

Value-specific transitions

In the last lesson we saw how we can customize the transition for our animations using the transition prop:

<motion.p
  initial={{ opacity: 0, height: 0 }}
  animate={{ opacity: 1, height: "auto" }}
  exit={{ opacity: 0, height: 0 }}
  transition={{ type: "spring", bounce: 0.5, duration: 1 }}
/>

If we want more control over how different properties (like opacity and height) behave, we can define different transitions by nesting their options under the corresponding property's name:

<motion.p
  initial={{ opacity: 0, height: 0 }}
  animate={{ opacity: 1, height: "auto" }}
  exit={{ opacity: 0, height: 0 }}
  transition={{
    opacity: { duration: 0.2 },
    height: { type: "spring", bounce: 0.5, duration: 1 },
  }}
/>

This example speeds up the duration on opacity so messages fade out faster than they move. Any properties not defined in the object receive the default transition.

Disabling initial mount animations

If we reload the page, we'll see all the messages run their mount animations. You often don't want this during your app's initial render, so we can disable this by setting the initial prop on AnimatePresence to false:

<AnimatePresence initial={false}>
  {[...messages].reverse().map((id) => (
    <motion.li
      initial={{ opacity: 0, height: 0 }}
      animate={{ opacity: 1, height: "auto" }}
      exit={{ opacity: 0, height: 0 }}
      key={id}
    >
      {/*  */}
    </motion.li>
  ))}
</AnimatePresence>

Now the initial render is no longer animated โ€“ just what we want!

Join Build UI Pro to access this video's summary and source code.

Join Build UI Pro

Watch every video, support our work, and get exclusive perks!

Build UI is the new home for all our ideas. It will eventually have hundreds of premium videos and a thriving community, but right now it's the early days.

If you like what you see and you've ever wanted to support our work, subscribe today and start enjoying all the perks of becoming a member!

$29/month

Watch everything. Cancel anytime.

What you'll get as a Build UI Pro member

Full access to all Build UI videos

Get full access to all of our premium video content, updated monthly.

Private Discord

Ask questions and get answers from Sam, Ryan and other pro members.

Video summaries with code snippets

Easily reference videos with text summaries and copyable code snippets.

Source code

View the source code for every video, right on GitHub.

Invoices and receipts

Get reimbursed from your employer for becoming a better coder!