Animating a check icon in React with Framer Motion

Animating a check icon in React with Framer Motion

Build a re-usable animated check icon component using Animate Presence and Motion Component

Introduction

Both React and Framer Motion are incredible technologies that help us build next-level user interfaces in a declarative way.

If you're wondering what declarative programming is, I highly recommend checking out the article Imperative vs Declarative Programming

In this article, we'll create a reusable check icon component that animates by drawing itself in and out when it's mounted and unmounted by React. The animation utilises some core components from Framer Motion which are also described.

Demo

Setting up the icon component

We'll use the check icon provided by hero icons. Since we'll need to manipulate the path within the check icon svg, let's copy the JSX from hero icons into a new component AnimatedCheckIcon:

function AnimatedCheckIcon() {
  return (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      fill="none"
      viewBox="0 0 24 24"
      strokeWidth={1.5}
      stroke="currentColor"
    >
      <path
        strokeLinecap="round"
        strokeLinejoin="round"
        d="M4.5 12.75l6 6 9-13.5"
      />
    </svg>
  );
}

Nothing special here at the moment (of course, apart from the beautiful icon by the hero icons team), so let's install framer motion in preparation for adding animations to this component

npm install framer-motion

Animating an SVG path with Framer Motion

The first step to animate a path element is converting it to a motion.path component. Motion components exist for all HTML and SVG elements and add animation super-powers to their DOM primitive counterparts.

These super-powers include defaults bundled with Framer Motion leading us to better looking and performant animations without worrying about the intricate details. The bare bones version to animate a motion component from one state to another requires passing the initial and animate prop specifying the styles for these states. Framer Motion then transitions between these states while providing satisfying defaults for its easing function and its duration at the least.

Declarative programming at its best!

Amongst these super-powers, is the ability to animate pathLength for a motion.path component.

Let's take a look at what happens when we use motion.path in our AnimatedCheckIcon instead of path and specify the initial and animate props to take us from a pathLength of 0 to 1:

<motion.path
  strokeLinecap="round"
  strokeLinejoin="round"
  initial={{ pathLength: 0 }}
  animate={{ pathLength: 1 }}
  d="M4.5 12.75l6 6 9-13.5"
/>

motion-path-animate.gif

Great! We're already animating the check icon drawing itself in.

Similarly, if we switch up the initial and animate pathLengths to go from 1 to 0, we'll see the check icon erase itself:

<motion.path
  strokeLinecap="round"
  strokeLinejoin="round"
  initial={{ pathLength: 1 }}
  animate={{ pathLength: 0 }}
  d="M4.5 12.75l6 6 9-13.5"
/>

motion-path-erase.gif

The transition duration in the GIFs has been increased from the default value for clarity but is not displayed in the code snippets as we'll be touching on that later in the article

Okay, so we now know how to animate the pathLength but what happens if we mount/unmount this component? Let's render this component based on some state that we can toggle with a button. So we can now conditionally render our <AnimatedCheckIcon /> like so:

{isCheckIconVisible && <AnimatedCheckIcon />}

motion-path-animation-initial.gif

We can see the path now animate when the component mounts but how do we specify an animation to run when it un-mounts?

Adding an unmount animation with Animate Presence

Framer motion provides the AnimatePresence component that helps us define animations that happen when a component is removed from the DOM by React.

This can be done by wrapping the component to be removed within <AnimatePresence /> and adding the exit prop to the motion component defining its animation behaviour.

To make use of AnimatePresence, first let's add an isVisible: boolean prop to our <AnimatedCheckIcon /> component that conditionally renders our icon based on the value provided:

function AnimatedCheckIcon({ isVisible = true }) {
  return (
    isVisible && (
      <svg
        xmlns="http://www.w3.org/2000/svg"
        fill="none"
        viewBox="0 0 24 24"
        strokeWidth={1.5}
        stroke="currentColor"
      >
        <motion.path
          strokeLinecap="round"
          strokeLinejoin="round"
          initial={{ pathLength: 0 }}
          animate={{ pathLength: 1 }}
          transition={{ duration: 0.5 }}
          d="M4.5 12.75l6 6 9-13.5"
        />
      </svg>
    )
  );
}

We now need to nest our conditionally rendered icon within <AnimatePresence />:

function AnimatedCheckIcon({ isVisible = true }) {
  return (
    <AnimatePresence>
      {isVisible && (
        <svg
          xmlns="http://www.w3.org/2000/svg"
          fill="none"
          viewBox="0 0 24 24"
          strokeWidth={1.5}
          stroke="currentColor"
        >
          <motion.path
            strokeLinecap="round"
            strokeLinejoin="round"
            initial={{ pathLength: 0 }}
            animate={{ pathLength: 1 }}
            transition={{ duration: 0.5 }}
            d="M4.5 12.75l6 6 9-13.5"
          />
        </svg>
      )}
    </AnimatePresence>
  );
}

It's necessary that <AnimatePresence> component is outside of the isVisible && condition as that allows Framer Motion to workaround React's limitation of not providing enough data to animate an element when it's being removed from the React tree

With our component wrapped in AnimatePresence, we can now add the exit prop to our motion.path:

<motion.path
  strokeLinecap="round"
  strokeLinejoin="round"
  initial={{ pathLength: 0 }}
  animate={{ pathLength: 1 }}
  exit={{ pathLength: 0 }}
  d="M4.5 12.75l6 6 9-13.5"
/>

We specify exit animation to complete with pathLength: 0 so the icon erases itself as we saw before. In summary, the props:

initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
exit={{ pathLength: 0 }}

declare that the component should transition from pathLength 0 to 1 when it mounts and 1 to 0 when it un-mounts.

If we connect the toggle state for rendering our icon from earlier but use the isVisible prop as follows:

{<AnimatedCheckIcon isVisible={isCheckIconVisible} />}

we now see the unmount animation too:

motion-path-moun-unmount-animation.gif

Disabling the initial mount animation

At this point, whenever our <AnimatedCheckIcon /> component first mounts, it will animate itself based on the value of the isVisible prop. This might be the behaviour that's desired within a certain use case but perhaps not in others.

Thankfully, the <AnimatePresence /> component exposes the initial prop that accepts a boolean value to handle this concern. Passing false suppresses the first mount animation while true, which is the default, runs the animation on the initial mount. Adding it as a prop to our icon component, to mimic this behaviour, our component interface now looks like the following:

function AnimatedCheckIcon({ initial = true, isVisible }) {
  return (
    <AnimatePresence initial={initial}>
      {/* Rest of the component */}
    </AnimatePresence>
  )
}

Fine-tuning the animation

Our animation is now set up but we can play around with yet another prop for Motion Components – transition – to make it even better! The transition prop can be used to fine-tune how the animation values go from one state to the other.

We'll make use of 3 properties within transition:

1. Type

The type property accepts multiple options but two worth highlighting are spring and tween. Spring animations use spring physics to transition between animation states while Tween is a duration-based type and can be customised with an easing function. Framer Motion picks the most appropriate defaults based on the styles being animated which in our case would be tween.

2. Duration

The duration property specifies how long the transition takes to go from the initial to the animated state, specified in seconds. The default duration for tween animations is 0.3 seconds.

3. Ease

The ease property accepts an easing function to customise how fast the values change while animating over the specified duration. We'll use the built-in ease-out easing when the check icon mounts and ease-in when it un-mounts. The ease-out function starts quick but ends in a relaxed fashion while the ease-in animation starts in a relaxed fashion and ends quick. Try reverting these values in the sandbox to see how they affect our animation!

Putting these customisations together:

<motion.path
  initial={{ pathLength: 0 }}
  animate={{ pathLength: 1 }}
  exit={{ pathLength: 0 }}
  transition={{
    type: "tween",
    duration: 0.3,
    ease: isVisible ? "easeOut" : "easeIn"
  }}
  strokeLinecap="round"
  strokeLinejoin="round"
  d="M4.5 12.75l6 6 9-13.5"
/>

we get the animation from the demo!

Our final reusable animated check icon component

So, here's what we end up with as our reusable component that you can drop into your project for that well-polished micro-interaction:

import { AnimatePresence, motion } from "framer-motion";

function AnimatedCheckIcon({ initial = true, isVisible }) {
  return (
    <AnimatePresence initial={initial}>
      {isVisible && (
        <svg
          xmlns="http://www.w3.org/2000/svg"
          fill="none"
          viewBox="0 0 24 24"
          strokeWidth={1.5}
          stroke="currentColor"
          className="CheckIcon"
        >
          <motion.path
            initial={{ pathLength: 0 }}
            animate={{ pathLength: 1 }}
            exit={{ pathLength: 0 }}
            transition={{
              type: "tween",
              duration: 0.3,
              ease: isVisible ? "easeOut" : "easeIn"
            }}
            strokeLinecap="round"
            strokeLinejoin="round"
            d="M4.5 12.75l6 6 9-13.5"
          />
        </svg>
      )}
    </AnimatePresence>
  );
}

animating-check-icon-path-framer-motion.gif

In case, you're curious about the toggle button animation on the demo sandbox, it uses Framer Motion gestures to animate its scale on hover and tap. You can check out the code sandbox for the full code and stay tuned for more articles on the topic.

Thanks for reading!