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"
/>
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"
/>
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 />}
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 theisVisible &&
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:
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>
);
}
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!