Animating SVGs with Framer Motion

Thumbnail cover image Animating SVGs with Framer Motion

There is nothing like a slick animation to bring some extra life to a web design, and using SVGs to render a logo or hero header as opposed to a standard png allows great specificity in choosing how to animate it.

Here I'll highlight some examples from my web design site, where I chose to animate my hero image/logo svg, along with some other icons on the home page. The logo for TC Web Design is simply the word 'design' in the Ogham alphabet, a script used to write the Irish language from around 400 - 1000 AD.

I wanted to do something interesting with the logo that would really bring attention to it when the page loads, so I had an idea to have each one of the characters of the script fly in at different intervals and rotations from random directions off screen, which each page load having different starting points. Thankfully, Framer Motion works quite well for this, and I was already using it for all of my other animations on the site because its natural and 'life-like' feel.

SVG to HTML and motion.path

Firstly I needed to extract the paths(each line of my svg) from the original and rewrite it in html, as I need access to each one in order to animate them. Second, we add the motion. prefix to the svg and path html element in order for Framer Motion to work on them. CSS for stroke (the path colour) and stroke-width etc are applied separately. I have shown the first few paths below:

      <motion.svg
        viewBox="-350 -150 900 1200"
        version="1.1"
        xmlns="http://www.w3.org/2000/svg"
      >
        <motion.path
          d="M83.561,4.251l0.523,917.179"
        />
        <motion.path
          d="M84.084,32.945l77.618,-0.125"
        />
        <motion.path
          d="M84.084,67.606l77.618,-0"
        />
        ......

Also note the viewBox attribute there, it provides the position of the paths that make up the svg and relative size of the paths in the svg element.

Framer Motion variants

With Framer Motion it is possible to easily apply the same animation attributes to multiple elements with variants - an object with predefined targets that can be applied to any element (it unlocks a few other animation possibilities as well, such as transition delays and durations). I have settled on creating 4 different variants to orchestrate this the way I want. The first flying in from a random location on the x plane, another on the y plane, and another two on x and y planes. These each have separate delays and rotations as well.

Here is an example of the xypath variant I have defined, which is applied to a few of the paths:

  const xypath = {
    hidden: {
      x: xyStart,
      y: yxStart,
      opacity: 0,
      rotate: 180,
    },
    visible: {
      rotate: 360,
      x: 0,
      y: 0,
      opacity: 1,
      transition: {
        delay: 0.9,
        duration: 2.5,
        type: "spring",
        // damping: 20,
        stiffness: 50,
      },
    },
  };

Here the hidden path starts in a random position on page load or when the button to toggle the animation is clicked, which I'll explain shortly. Additionally there is an opacity of 0 to hide it and a rotation of 180 degrees to go back to hidden from its visible state. The visible state also includes the original position of the paths to be animated to, opacity and some transition parameters that I found to be visually appealing.

Random starting point, useState and useEffect

In order to calculate a random number within a range for the starting points each time the button is pressed to begin the animation, I have set up a React useState hook for each, along with a React useEffect hook to refresh those values. That hook is also setting the animation active with a framer motion hook called useAnitmationControls, which allows a convenient way to start the animation when the button is pressed by setting the animate attribute in each path element to the variable I have set (controls).

function generateRandomInteger(min, max) {
  return Math.floor(min + Math.random() * (max - min + 1));
}

export const Ogham = () => {
  const [clicked, setClicked] = useState(false);
  const controls = useAnimationControls();
  const values = [-1000, 1000];
  const [yStart, setYStart] = useState(generateRandomInteger(values[0],values[1]));
  const [xStart, setXStart] = useState(generateRandomInteger(values[0],values[1]));
  const [xyStart, setXyStart] = useState(generateRandomInteger(values[0],values[1]));
  const [yxStart, setYxStart] = useState(generateRandomInteger(values[0],values[1]));

  useEffect(() => {
    if (clicked) {
      setYStart(generateRandomInteger(values[0],values[1]));
      setXStart(generateRandomInteger(values[0],values[1]));
      setYxStart(generateRandomInteger(values[0],values[1]));
      setXyStart(generateRandomInteger(values[0],values[1]));
      controls.start("visible");
    } else {
      controls.start("hidden");
    }
  }, [clicked]);

    return (
    <div>
      <button
        onClick={() => setClicked(!clicked)}
      >
        Toggle animation below:
      </button>
     ........

Putting it all together

Finally I'll apply the animate and initial attributes to each path as well as pass in the variants object I am using for that specific path. Here is a section of the final code:

 <motion.svg
        className="shadow-md"
        initial="hidden"
        viewBox="-350 -150 900 1200"
        version="1.1"
        xmlns="http://www.w3.org/2000/svg"
      >
        <motion.path
          variants={ypath}
          initial="hidden"
          animate={controls}
          d="M83.561,4.251l0.523,917.179"
        />
        <motion.path
          variants={xpath}
          initial="hidden"
          animate={controls}
          d="M84.084,32.945l77.618,-0.125"
        />
        <motion.path
          variants={xpath}
          initial="hidden"
          animate={controls}
          d="M84.084,67.606l77.618,-0"
        />
        <motion.path
          variants={xypath}
          initial="hidden"
          animate={controls}
          d="M84.084,102.122l77.618,0.391"
        />
        <motion.path
          variants={yxpath}
          initial="hidden"
          animate={controls}
          d="M84.084,136.942l77.618,0.106"
        />

  .....

And thats it for that animation! Here is gist on github with the full component.

Bonus: Animating SVG path trace

To highlight how flexible Framer Motion is, here is another example - this time I'm tracing the svg path with pathLength. Applied in the same way as before, with variants - giving me access to the transition duration as well as keeping things tidy.

import { motion, useAnimation, useAnimationControls } from "framer-motion";
import { reactPath } from "./svg-paths";
import React, { useEffect, useState } from "react";

export const ReactAnimation = (s) => {
  const controls = useAnimationControls();
  const [clicked, setClicked] = useState(false);

  const variants = {
    hidden: {
      pathLength: 0,
      transition: {
        duration: 1,
      },
    },
    visible: {
      pathLength: 1,
      transition: {
        duration: 4,
      },
    },
  };

  useEffect(() => {
    if (clicked) {
      controls.start("visible");
    } else {
      controls.start("hidden");
    }
  }, [clicked]);
  return (
    <div>
      <button
        onClick={() => setClicked(!clicked)}
      >
        Toggle animation below:
      </button>
      <motion.svg
        fill={"#606C38"}
        animate={controls}
        xmlns="http://www.w3.org/2000/svg"
        viewBox="-250 -250 1000 1000"
      >
        <motion.path
          animate={controls}
          initial="hidden"
          variants={variants}
          d={reactPath}
        />
      </motion.svg>
    </div>
  );
};

Conclusion

Framer Motion is a wonderful tool that makes complex animations wonderfully easy, while greatly expanding on the standard CSS animations, making them more life like. The spring animation used in the Ogham svg for example has a delightful bounce to it that I'm not sure could be achieved any other way. And the other animations on the web design site have a great life-like feel to the way I was able to configure them to fly onto the page.

Thanks for reading! As always, I am always open to any comments or questions - email me!

PS - I used this post as an excuse to get .mdx files integrated into the blog engine on this site (running NextJS). I was able to put the components for those animations directly into my markdown file as a normal blog post. Great!

Written by Tom Caraher on 2023-03-12