SVG Morphing Transitions: Creating Smooth Icon Changes
Learn how to morph one SVG shape into another. Discover the rules of path morphing and how to use tools like Flubber or Framer Motion for complex transitions.
One of the most captivating effects in UI design is SVG Morphing—where one icon fluidly transforms into another (e.g., a “Play” triangle morphing into a “Pause” double-bar, or a “Hamburger” menu morphing into a “Close” X).
Unlike simple scaling or rotation, morphing requires interpolating the mathematical coordinates of the SVG paths themselves.
Here is everything you need to know about creating smooth SVG morphing transitions.
The Golden Rule of Native Morphing
If you want to morph two paths natively using CSS or simple JavaScript, there is one unbreakable rule: Both paths must have the exact same number of points, and the commands must match in sequence.
Why does this matter?
A browser knows how to animate a point moving from X: 10 to X: 50. But if Path A has 4 points (a square) and Path B has 3 points (a triangle), the browser doesn’t know what to do with the 4th point. It will simply snap from one shape to the other without animating.
Example: The Perfect Morph (Play to Stop)
Let’s morph a Play icon (a triangle) into a Stop icon (a square). We must draw the triangle using 4 points (with two points overlapping) so it matches the square’s 4 points.
<svg viewBox="0 0 100 100">
<path id="morphPath" d="M10,10 L90,50 L90,50 L10,90 Z" fill="black" />
<!-- A triangle, but drawn with 4 points to match a square -->
</svg>
You can then use CSS to transition the d attribute on hover:
#morphPath {
transition: d 0.3s ease-in-out;
}
svg:hover #morphPath {
/* Morph into a square */
d: path("M10,10 L90,10 L90,90 L10,90 Z");
}
Animating the d attribute via CSS is supported in most modern browsers, but for complex paths, it can be slightly jerky. For production apps, JS libraries are preferred.
Complex Morphing: Handling Different Point Counts
In reality, you rarely design a “hamburger” menu with the exact same number of Bezier curve points as a “close” icon.
When paths are fundamentally incompatible, you must use a JavaScript library designed to calculate the missing points and create an interpolation function.
Tool 1: Flubber (Best for Pure JS/Vanilla)
Flubber is a fantastic utility library specifically designed to solve the “shape interpolation” problem. It calculates the smoothest possible transition between two incompatible SVG paths.
import { interpolate } from "flubber";
const trianglePath = "M... ";
const squarePath = "M... ";
// Flubber creates an interpolation function
const morph = interpolate(trianglePath, squarePath);
// You can pass a value between 0 and 1 to get the exact path string at that moment
morph(0); // Returns triangle string
morph(0.5); // Returns halfway morphed string
morph(1); // Returns square string
You can hook this interpolation function up to requestAnimationFrame to animate the transition.
Tool 2: Framer Motion (Best for React)
If you are using React, Framer Motion makes morphing relatively painless, provided the paths are somewhat compatible or you are willing to use multiple paths.
To animate a hamburger menu into an ‘X’, the easiest method is not to morph a single path, but to animate three distinct lines (the hamburger bars) into the intersecting lines of the X.
import { motion } from "framer-motion";
import { useState } from "react";
export const MenuToggle = () => {
const [isOpen, setIsOpen] = useState(false);
return (
<button onClick={() => setIsOpen(!isOpen)} className="w-12 h-12 relative">
<motion.svg viewBox="0 0 24 24">
{/* Top line of hamburger -> Top-left to bottom-right of X */}
<motion.path
stroke="black" strokeWidth="2"
animate={isOpen ? "open" : "closed"}
variants={{
closed: { d: "M4 6h16" },
open: { d: "M6 6l12 12" }
}}
/>
{/* Middle line -> Fades out */}
<motion.path
stroke="black" strokeWidth="2"
animate={isOpen ? "open" : "closed"}
variants={{
closed: { d: "M4 12h16", opacity: 1 },
open: { d: "M4 12h16", opacity: 0 }
}}
/>
{/* Bottom line -> Bottom-left to top-right of X */}
<motion.path
stroke="black" strokeWidth="2"
animate={isOpen ? "open" : "closed"}
variants={{
closed: { d: "M4 18h16" },
open: { d: "M6 18l12 -12" }
}}
/>
</motion.svg>
</button>
);
};
This approach circumvents the complex mathematics of path interpolation entirely, relying on simple transform and opacity changes which perform incredibly well.
Summary
SVG morphing is powerful but mathematically complex.
- For simple shapes: Ensure both paths have the exact same number of points and use CSS transition on the
dattribute. - For complex incompatible shapes: Use an interpolation library like
Flubber. - For React components (like Menus): Often, the best “morph” isn’t a morph at all, but rather animating the translation, rotation, and opacity of individual paths using Framer Motion.