Skip Navigation
Blog cover image

Animating SVGs with Motion

Avatar of yogeshbhutkar yogeshbhutkar

2 min read (5 min read total)

#React #Motion #SVG #Animation #UI

Introduction

SVGs (Scalable Vector Graphics) are a powerful way to create crisp and scalable images for the web. When combined with animation, they can bring your UI to life and create engaging user experiences. In this article, we will explore how to animate SVGs using Motion, a popular library for creating smooth animations.

We will create a simple animated SVG component that animates the GitHub logo. Here’s a demo of what we will be building:

Github LogoHover me

Setting Up Motion

To get started, install Motion in your project:

bun add motion

The SVG

The GitHub logo is made up of two <path> elements — the body outline and the tail. We’ll animate them independently, so keep them as separate paths.

<svg
  xmlns="http://www.w3.org/2000/svg"
  width="56"
  height="56"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  strokeWidth="1.5"
  strokeLinecap="butt"
  strokeLinejoin="round"
>
  {/* Body */}
  <path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4" />

  {/* Tail */}
  <path d="M9 18c-4.51 2-5-2-7-2" />
</svg>

Note the strokeLinecap="butt" on the SVG. This is important — the default round cap renders a visible dot at the start of each path even when pathLength is 0, which causes both paths to appear before the animation begins.

Drawing with pathLength

Motion lets you animate pathLength from 0 to 1 — effectively drawing a stroke along the path over time. Wrap each <path> with motion.path and set initial={{ pathLength: 0 }}.

The body draws first over 2 seconds. The tail waits for it to finish using delay: 2, then draws quickly in 0.5 seconds.

// Body draws over 2 seconds
const outlineVariants: Variants = {
  rest: { pathLength: 0 },
  hover: {
    pathLength: 1,
    transition: { duration: 2, ease: "easeInOut" },
  },
};

// Tail draws after the body finishes
const tailVariants: Variants = {
  rest: { pathLength: 0, rotate: 0 },
  hover: {
    pathLength: 1,
    rotate: 15,
    transition: {
      pathLength: { duration: 0.5, delay: 2, ease: "easeInOut" },
    },
  },
};

Wiggling the Tail

Once the tail is drawn, we want it to wiggle. We add a rotate animation that starts after the tail has fully appeared (delay: 2.5 = 2s body + 0.5s tail draw).

Using a spring transition with repeatType: "mirror" gives it a natural, oscillating feel — like a real wag.

const tailVariants: Variants = {
  rest: { pathLength: 0, rotate: 0 },
  hover: {
    pathLength: 1,
    rotate: 15,
    transition: {
      pathLength: { duration: 0.5, delay: 2, ease: "easeInOut" },
      rotate: {
        delay: 2.5,
        repeat: Infinity,
        repeatType: "mirror",
        type: "spring",
        stiffness: 100,
        damping: 10,
      },
    },
  },
};

Orchestrating with Variants

Instead of managing hover state manually with useState, we use Motion’s variants system. Variants let you define named animation states and propagate them through a component tree automatically.

We define "rest" and "hover" states on a motion.div wrapper:

<motion.div
  initial="rest"
  whileHover="hover"
>
  <motion.path variants={outlineVariants} />
  <motion.path variants={tailVariants} />
</motion.div>

When the user hovers the motion.div, Motion automatically applies the "hover" variant to all children that have variants defined — no need to pass animate down to each child manually.

We also use the same system to fade out a “Hover me” hint when the animation starts:

const hintVariants: Variants = {
  rest: { opacity: 1 },
  hover: { opacity: 0, transition: { duration: 0.2 } },
};

Putting It All Together

Here’s the complete component: