Create An Animated SVG Icon With Motion.JS

Create An Animated SVG Icon With Motion.JS

In this tutorial, we'll create a simple checkbox that transforms from a circle to a tick. As we create this we'll learn a little bit about motion.js and a little about SVGs too. You will need basic knowledge of React to follow along

Check out the finished code on Codesandbox here

1. Create The Icon

I created this icon in Figma, but you could use any vector-based design program (e.g. Adobe Illustrator or Affinity Designer) You can even create the SVG code from scratch if you're an SVG pro. It's worth knowing how SVG code works even if you do use a design program as you often need to clean the code up a little. If you don't have any design software you can use my SVG code below.

Create a new 20x20 artboard and draw these three paths all-paths.png

When these three paths are overlaid your artboard should look like this single-artboard.png

The advantage of creating this checkbox with three separate paths is that we can remove a path to create the checked and unchecked options. So we'll either be showing the A+B paths or the A+C paths as shown below. combinations.png

A couple of things to remember when creating your checkbox.

  • Use the path tool! we'll be animating the length of the path so don't use squares and circles. use actual paths.
  • When you create a path the first point you create will be where the animation will start from.

To save as an SVG in Figma:

  1. Right-click on the artboard in the layers panel
  2. Select Copy/Paste as
  3. Select Copy as SVG

you can now paste this code directly into HTML. It should look something like this.

<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
  <path 
    d="M10 1C5.02944 1 1 5.02944 1 10C1 14.9706 5.02944 19 10 19C14.9706 19 19 14.9706 19 10C19 9.66199 18.9814 9.32833 18.9451 9C18.9298 8.8614 18.9113 8.72374 18.8897 8.58711" 
    stroke="black" 
    stroke-width="2"/>
  <path 
    d="M17 4.4L10 12L7 8" 
    stroke="black" 
    stroke-width="2" />
  <path 
    d="M19 10C19 5.02944 14.9706 1 10 1" 
    stroke="black" 
    stroke-width="2"/>
</svg>

note: I've kept the paths in the order A,B,C as shown in the first image of this article to make it easier to follow. I'll call them Path A, Path B and Path C

We can make some modifications to this SVG code:

  • Firstly we can remove the width and height arguments. By removing them from the SVG code we can control them using CSS instead. The exact position of the points in our paths will be relative to the viewBox argument, so we can scale our icon to be as big or small as we want without changing the icon.
  • We can replace stroke="black" with stroke="currentColor" this way we can control the colour of the path stroke directly in CSS too. We could even make it change color on hover etc... in CSS if we like.
  • We should add fill="none" to each path. We don't want the paths to ever have a fill color, so if we explicitly tell it to have no fill we won't accidentally add it in with CSS or any other way.
  • If we like we can change the thickness of the path with the stroke-width argument. but let's leave it at 2 for now.
  • And we can give the paths a rounded end by adding strokeLinecap="round"
  • d is the coordinate of each path of the point. I wouldn't touch those. but you can try making small tweaks to these numbers, just to see how it changes the icon.

now after some manual adjustments, we have some SVG code that looks like this. Feel free to copy it and use it yourself if you don't have any design software. Or if you want to skip this first step.

<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
  <path
    fill="none"
    strokeWidth="2"
    strokeLinecap="round"
    stroke="currentColor"
    d="M10 1C5.02944 1 1 5.02944 1 10C1 14.9706 5.02944 19 10 19C14.9706 19 19 14.9706 19 10C19 9.66199 18.9814 9.32833 18.9451 9C18.9298 8.8614 18.9113 8.72374 18.8897 8.58711" />
  <path
    fill="none"
    strokeWidth="2"
    stroke="currentColor"
    strokeLinecap="round"
    d="M17 4.4L10 12L7 8" />
  <path
    fill="none"
    strokeWidth="2"
    stroke="currentColor"
    strokeLinecap="round"
    d="M19 10C19 5.02944 14.9706 1 10 1" />
</svg>

Now let's add our SVG to a React project

2. Create A Checkbox Component in React

Create a new React project (or open an existing one) and add a new component called Checkbox.jsx now let's add our SVG code into this file like this

export default function Checkbox() {
  return <svg ...>
     ...
  </svg>
}

cool. Step 2 complete! but first, let's see how we would switch between the checked and unchecked states without any animation.

First, we can import useState, create a boolean state for checked and change the state when we click the svg. Then our code will look like this

// import useState
import { useState } from "react";

export default function Checkbox() {

  // booled state called `checked` and set it to false
  const [checked, setChecked] = useState(false);

  // function to change the checked state on click
  const handleClick = () => {
    setChecked((prev) => !prev);
  };

  // bind the handleClick function to the svg element
  return <svg onClick={handleClick} ...>
     ...
  </svg>
}

Now we can switch between the two icon states by conditionally hiding either path B or path C depending on the react state. Our code will now look like this

import { useState } from "react";

export default function Checkbox() {

  const [checked, setChecked] = useState(false);

  const handleClick = () => {
    setChecked((prev) => !prev);
  };

  return <svg onClick={handleClick} viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
    <path
      fill="none"
      strokeWidth="2"
      strokeLinecap="round"
      stroke="currentColor"
      d="M10 1C5.02944 1 1 5.02944 1 10C1 14.9706 5.02944 19 10 19C14.9706 19 19 14.9706 19 10C19 9.66199 18.9814 9.32833 18.9451 9C18.9298 8.8614 18.9113 8.72374 18.8897 8.58711" />
    {/* only show path B when checked */}
    {checked && 
      <path
        fill="none"
        strokeWidth="2"
        stroke="currentColor"
        strokeLinecap="round"
        d="M17 4.4L10 12L7 8" />
    }
    {/* only show path C when not checked */}
    {!checked &&
      <path
        fill="none"
        strokeWidth="2"
        stroke="currentColor"
        strokeLinecap="round"
        d="M19 10C19 5.02944 14.9706 1 10 1" />
    }
  </svg>
}

If you import this component into a react project you'll see an icon that changes when you click on it. yay! but it doesn't animate yet! booo!

Also, it's huge. Go ahead and add some style to control the size of your icon.

<svg style={{ width:60 }} ... >

this will work. or you could create a CSS class

3. Animate The Icon Already!

now we'll add framer motion to our project by typing this in the terminal npm i framer-motion

now let's import this at the top of our component like this

import { useState } from "react";
import { motion } from "framer-motion";

export default function Checkbox() {
   ...
}

Now you can turn any HTML or SVG element into a motion element. A motion element works exactly the same as the existing HTML or SVG element but you can pass some awesome new props for animations and gestures. Let's try that now. To convert a path element we simply add motion. like this motion.path So if we convert path B and path C into motion elements (remember A doesn't animate) our code will look like this:

import { useState } from "react";
import { motion } from "framer-motion";

export default function Checkbox() {
  ...
  return <svg onClick={handleClick} viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
    <path ... />
    <motion.path ... />
    <motion.path ... />
  </svg>
}

That's great. We now have two motion elements. But they don't animate yet. Let's send them both some animate props and see some magic. Animate props work just like style props, you can use it to add styles and those styles will animate in. There are some fun ones too like rotate or scale

Let's try a couple of crazy ones out just for fun. Notice we now have a rotate animation on path B and a scale animation on path C

Run this code and check it out:

import { useState } from "react";
import { motion } from "framer-motion";

export default function Checkbox() {

  const [checked, setChecked] = useState(false);

  const handleClick = () => {
    setChecked((prev) => !prev);
  };

  return <svg onClick={handleClick} viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
    <path
      fill="none"
      strokeWidth="2"
      strokeLinecap="round"
      stroke="currentColor"
      d="M10 1C5.02944 1 1 5.02944 1 10C1 14.9706 5.02944 19 10 19C14.9706 19 19 14.9706 19 10C19 9.66199 18.9814 9.32833 18.9451 9C18.9298 8.8614 18.9113 8.72374 18.8897 8.58711" />
    <motion.path
      fill="none"
      strokeWidth="2"
      stroke="currentColor"
      strokeLinecap="round"
      animate={{ rotate: 45 }}
      d="M17 4.4L10 12L7 8" />
    <motion.path
      fill="none"
      strokeWidth="2"
      stroke="currentColor"
      strokeLinecap="round"
      animate={{ sclae:0.2 }}
      d="M19 10C19 5.02944 14.9706 1 10 1" />
  </svg>
}

You'll notice it now runs the animation only once on page load. We can make the animations conditional by adding a simple ternary operator like this:

...

export default function Checkbox() {

  ...

  return <svg onClick={handleClick} viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
    ...
    <motion.path
      ...
      animate={{ rotate: checked ? 0 : 45 }}
      ...>
    <motion.path
      ...
      animate={{ scale: checked ? 1 : 0 }}
      ... />
  </svg>
}

now you can see the two crazy animations take place when you click the SVG. Well, let's get back to animating the paths in and out. We can animate in and out the pathLength of the SVG paths and we should get the desired effect. give this a try:

  ...
    <motion.path
      ...
      animate={{ pathLength: checked ? 1 : 0 }}
      ...>
    <motion.path
      ...
      animate={{ pathLength: checked ? 0 : 1 }}
      ... />
  ...

Pretty cool, right? Our icon now animates. You can see the finished thing on Codesandbox here

From here you can try changing the duration or the easing of the animation using the transition props.

Check out Framer's awesome documentation here

Or let me know if you'd like me to make a more complex tutorial.