How to Animate SVG Paths with Framer Motion

How to Animate SVG Paths with Framer Motion

Featured on Hashnode

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 container

  • a <path> forming a rectangle and spanning the size of the container

  • a <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!

Did you find this article valuable?

Support Noël's Blog by becoming a sponsor. Any amount is appreciated!