If you're looking for some flair when fading in a logo or want to give loading animations some spice, SVG path animations are just the thing! Framer motion makes it easy for you to create great looking path animations for your React project.
Let's see how path animations work!
SVG Paths
Paths are by far the most powerful SVG element. They can create arcs, curves, lines and more. If you used Photoshop, Illustrator or Inkscape, you are likely already familiar with paths. It is the SVG equivalent of the pen tool.
It draws a line on the canvas, moving from point to point. The d
attribute controls where that line goes. It uses commands and parameters passed as a string. The path can move straight or curved. It can move relative to its current position, and it can move in absolute terms. The coordinates in the d
parameter are always unitless. That means they are in the user coordinate system established in the parent <svg>
component. If you don't know what that is, I wrote a blog post with interactive examples that covers this in detail.
I won't go into further detail on this because the MDN docs on paths are genuinely excellent.
Beziér curves are part of what makes paths so potent. If you find them just as fascinating as I do, check out Freya Holmér's video on Bézier Curves.
How Path Animations Work
One can use a clever trick to get the effect of the path being drawn. To animate the path, we need to control two attributes of the <path>
element.
The first is stroke-dasharray
, which turns our path into a dashed line. We can define a repeating pattern that describes how large the dashes and gaps between them are. Setting a value of "10 10"
would represent a dash that is ten units long, followed by a gap of 10 units long. This pattern repeats for the length of the path.
The second is stroke-dashoffset
, which pushes our pattern by a given amount along the path.
Now for our trick: We can make the dashes and gaps as long as the path itself. If we then use stroke-dashoffset
to move the pattern, we can completely hide the dash and only show the gap.
stroke-dashoffset
can then be animated to make the path draw itself. Click on the interactive window below to see for yourself.
To accurately hide the path, we usually need to get the total length of the path using something like this:
var path = document.querySelector('.path');
var length = path.getTotalLength();
But this is not necessary when we use Framer Motion. Although you can animate all these values, motion components expose unique attributes that allow us to do all of the above with a single value.
Let's get into it!
Installing Framer Motion
Navigate to the root directory of your React project (React 18 or greater). Run the following command.
npm install framer-motion
After installation, import Framer Motion wherever you want to use it.
import { motion } from "framer-motion"
Check out the installation docs for more information.
Convert <path>
to a Motion Component
Framer Motion gives us some valuable shortcuts for animating SVG paths. We need to convert our existing components into motion componentsto use them. We can do this by importing motion
at the top of the document and adding motion.
to the HTML element we want to animate. Don't forget to add motion.
to the closing tags.
import { motion } from "framer-motion";
<motion.svg>
<motion.path />
</motion.svg>
Now that we have motion components, we can start animating!
Animating the motion component
Luckily, Framer Motion makes this very simple. The <motion.path>
motion component has an attribute called pathLength
. As the name suggests, it controls the length of the visible path. A value of 0
completely hides the path, and a value of 1
shows the whole path.
Let's see how that looks in the following example. Press "Refresh Preview" on the CodeSandbox below to trigger the animation again. If you are unfamiliar with CodeSandbox, you can view the code by dragging the slider on the left side of the window.
By setting the initial={{pathLength: 0}}
and the a animate={{pathLength: 1}}
, the path animates from completely hidden to completely visible. Framer motion animates this every time the component is rendered.
The initial
attribute defines the animation target (state) before the animation. The animate
attribute defines the animation target that the component should animate to (the destination). This includes the final resting state of the motion component as well as the transition to get there.
All of the stroke-dasharray
and stroke-dashoffset
shenanigans we discussed earlier are handled under the hood by Framer Motion.
Transitions
Framer Motion has excellent default transitions, making it very fast to slap an animation on an unsuspecting HTML element 👏. But if we want more control, the transition
attribute is where we make that happen.
Most importantly, duration
allows us to define how long the animation takes from start to end (in seconds). We can let the animation play at a later time using delay
.
In the example, we are animating pathLength
from an initial value of 0
to a value of 1
. You may have noticed that the animation starts slowly, speeds up, and ends slowly. In animation, this is called easing and can control it via the ease
option on the transition
attribute. For this animation, Framer Motion defaults to "easeInOut"
. In contrast, the "linear"
value would keep the same speed between 0
and 1
. Framer Motion covers most common cases with their built-in easing functions. You can even define custom easing functions, but that is for another time.
There are also other transition types controlled by the type
option. These include tween, spring and inertia. Framer Motion will default to different types for different animations. The example above and any other animation that includes an easing function is of type tween. Spring and inertia work by simulating physics and thus have different controls.
You don't need to know all of this to make beautiful animations. I wanted to mention it so that you know what's possible and where to go from here. Let's look at examples of how we can use SVG path animations on the web!
Basic Examples
Hint: Click "Refresh preview" in the bottom left to see the "Inertia" example animate again.
Explanations
Forward
The same animation we did above.
Back
Instead of animating pathLength
from 0
to 1
, we do it from 1
to 0
.
Back to front
We leave pathLength
at 1
and instead animate pathOffset
from 1
to 0
.
Front to back
Same as back to front but moving pathOffset
from 0
to 1
.
Dashed
Just like with stroke-dasharray
, we can change the size of the gaps using Framer Motion's pathSpacing
attribute. It works the same as pathLength
. A number between 0
and 1
will determine how large the gap is.
Spring
Here we use the transition type spring
instead of tween
. There are two ways to configure it, but the easy one includes setting bounce
and duration
values. You can find out more here.
Inertia
The inertia
transition type is used mainly on drag animations and momentum scrolling. There are a lot of values to tweak, and if you are interested, I suggest you read more here.
Looping
Typically, the animation runs one time when the component renders. However, we can repeat this animation. Looping an animation is handled in the transition
attribute. You can set the repeat
option to the number of times you want the animation to play. I set it to Infinity
in the examples, so they loop forever. You can also change the repeatType
to affect the repeat behaviour. "loop"
will start the animation from the start. That is also what I am using above. repeatType: "mirror"
will switch the "to" and "from". repeatType: "reverse"
alternates forward and backwards playback.
Variants
So far, we have looked at initial
and animate
as our two animation targets. An animation target defines what the component should animate to and what transition it should use to get there.
With variants, we can set animate
to one of many targets, depending on the state of our application. Think of a menu that can be open or closed.
Variants allow us to bundle all of these targets into a single object. We can then choose which target to display by setting the animate
value to the key of the desired target within the variants object.
import { motion } from "framer-motion";
const menuVariants = {
open: {
opacity: 1,
transition: {
duration: 1
}
}
closed: {
opacity: 0,
transition: {
duration: 1
}
}
}
export default function Menu(){
return(
<motion.svg
variants={menuVariants}
initial="closed"
animate="open"
/>
)
}
To change the target, we can use state and conditionals:
// Using state with variant name
export default function Menu(){
const [showMenu, setShowMenu] = useState("closed")
return(
<motion.SVG
variants={menuVariants}
initial="closed"
animate={showMenu}
/>
)
}
// Using boolean state
export default function Menu(){
const [showMenu, setShowMenu] = useState(false)
return(
<motion.svg
variants={menuVariants}
initial="closed"
animate={showMenu ? "open" : "closed"}
/>
)
}
There are many use cases for variants, including orchestration, that I will not be getting into here. Let's look at advanced examples to see what we can do with variants.
Advanced Examples
Explanations
Logo
Believe it or not, this is the same animation as the "Forward" example from the "basic" section. No fancy variants or anything. I put it here to show you that animations don't need to be complicated to be impactful.
It's only a simple pathLength
animation going from 0
to 1
. The only difference is the <path>
I was using. The more interesting the shape of the <path>
, the more interesting the animation. In this case, I applied the animation to <motion.line>
, <motion.rect>
, <motion.circle>
and <motion.polygon>
elements.
You might be saying, "but these are not paths!". You are right, but it doesn't matter. It works just the same. All SVG lines are just vectors going from one coordinate of the viewBox to the next and can therefore be animated this way. If you want to know more about how SVGs use coordinates, check out my guide to SVG scaling.
Toggle
There are two <path>
's here. One is for the larger pill shape acting as the background. The other one is the moving pill shape in the foreground. Both <path>
elements are just a single line. A single fat line. To make them look pill-shaped, I increased the strokeWidth
and set strokeLinecap="rounded"
. I used state to switch between on and off variants. When on, I increase pathOffset
and change the colour to orange. Note that I am also using a new easing function.
Hover Button
Okay, this one is a bit more complex. I am using three elements:
a
<div>
that acts as a containera
<path>
forming a rectangle and spanning the size of the containera
<p>
that displays the text
I used whileHover
to detect the hover event. whileHover
is one of Framer Motion's gestures. Gestures allow you to define animation targets for one of several events (like hover, focus, scroll etc.). However, applying the gesture to the <path>
directly will not work because the pathLength
is 0
and, therefore, is hard to hover over.
To work around this, I applied whileHover
to the parent <div>
element. Motion components inherit initial
and animate
attributes from their parent components. Thus if we do not set those attributes on our <path>
element, the animation target from <div>
will be inherited.
Using variants, I defined the animation targets "hover"
and "default"
. While hovered, the pathLength
moves to 1
, the strokeWidth
increases and the color
changes to orange.
Although the <p>
element uses a different variants object, I used the same names for the targets ("hover"
and "default"
). Because <p>
is also a child of our parent <div>
, it can also inherit the animate
attribute.
Hovering the parent <div>
will activate different animations in the text and the path using the same animate
values. Isn't that cool?
A quick note on SSR
If you plan on server-side rendering your application, add strokeDasharray="0 1"
to your motion component. Framer Motion needs JavaScript to measure the total length of the path for most path animations. Because of that, the element is loaded onto the DOM before the animation can hide the element. This causes undesirable flickering on page loads. Setting strokeDasharray="0 1"
ensures that the path is hidden before the first render.
Conclusion
We've covered a lot of ground in this article. We discovered how path animations work, how to implement path animations with framer motion, transitions, motion components, variants and some advanced techniques with inheritance. Play around with what you have learned, and share with me what you came up with!
If you want to support me, the best way is to check out my new app at https://www.covercraft.io/. If you're struggling to find the right words for your cover letter or get tired of re-formulating how great your experience matches the job description, look no further. I appreciate your support!