A client asked us to add smooth icon morphing to their site: a circle turns into a star, the star into a heart. The design was ready, but out-of-the-box SVG morphing didn't work — the shapes jerked and broke. The reason: the start and end shapes had different numbers of nodes. We solved this with GSAP MorphSVGPlugin by GreenSock. Here's how to set up morphing without headaches and save your budget on debugging. Over 5 years, we've delivered over 50 projects with SVG animation, and morphing is one of the most common tasks. Below are proven tools, ready snippets, and typical pitfalls.
Problems We Solve
The main difficulty is mismatched point counts in path elements. SMIL and CSS clip-path only work with identical path structures. The second issue is performance on older devices: animation drops below 30 FPS. The third is complexity of integration into modern frameworks, especially with SSR/SSG, where client-side animations require careful handling. For instance, on one project we faced logo morphing in a catalog — unoptimized paths caused stuttering on iPhone 8. Our solution: move everything to GSAP with MorphSVG, and conversion to the product card increased by 15%.
How to Choose a Morphing Tool
| Tool | Complexity | Cost | Performance | Point Alignment |
|---|---|---|---|---|
| GSAP MorphSVG | Medium | Paid | High | Automatic |
| Flubber.js | Low | Free | Medium | Manual only |
| SMIL | High | Free | Low | Requires identical paths |
Calculate the cost of your project — contact us.
Why GSAP MorphSVG Is Better Than Alternatives
GSAP MorphSVGPlugin is 3x faster to set up for complex paths than Flubber.js. It automatically aligns node counts and supports timelines for sequences. In a product catalog project, we implemented brand logo morphing using React 18 and GSAP. The hover animation increased conversion to the product card by 15%.
Implementation on React with GSAP
We use React 18, Next.js 14, TypeScript. For morphing — GSAP with MorphSVGPlugin. Example implementation:
// lib/gsap-morph.ts
import { gsap } from 'gsap'
import { MorphSVGPlugin } from 'gsap/MorphSVGPlugin'
gsap.registerPlugin(MorphSVGPlugin)
export { gsap }
// components/MorphingIcon.tsx
import { useEffect, useRef, useState } from 'react'
import { gsap } from '../lib/gsap-morph'
const shapes = {
circle: 'M 50,10 A 40,40 0 1,1 49.9,10',
star: 'M50,5 L61,35 L95,35 L68,57 L79,91 L50,70 L21,91 L32,57 L5,35 L39,35 Z',
heart: 'M50,85 C10,60 5,30 25,15 C35,7 45,10 50,18 C55,10 65,7 75,15 C95,30 90,60 50,85 Z',
arrow: 'M 10,50 L 40,20 L 40,35 L 90,35 L 90,65 L 40,65 L 40,80 Z',
}
type ShapeKey = keyof typeof shapes
export function MorphingIcon() {
const pathRef = useRef<SVGPathElement>(null)
const [current, setCurrent] = useState<ShapeKey>('circle')
const morphTo = (target: ShapeKey) => {
if (!pathRef.current || target === current) return
gsap.to(pathRef.current, {
morphSVG: {
shape: shapes[target],
origin: '50% 50%',
type: 'rotational',
},
duration: 0.8,
ease: 'power2.inOut',
})
setCurrent(target)
}
return (
<div>
<svg viewBox="0 0 100 100" className="w-32 h-32">
<path
ref={pathRef}
d={shapes.circle}
fill="none"
stroke="#3b82f6"
strokeWidth="2"
/>
</svg>
<div className="flex gap-2 mt-4">
{(Object.keys(shapes) as ShapeKey[]).map(key => (
<button
key={key}
onClick={() => morphTo(key)}
className={`px-3 py-1 text-sm rounded ${
current === key ? 'bg-blue-500 text-white' : 'bg-gray-100'
}`}
>
{key}
</button>
))}
</div>
</div>
)
}
Alternative Tools: Flubber.js and SMIL
For simple two-state morphs, Flubber.js works well. It's free but requires manual point alignment. Install with npm install flubber. Example usage below:
// components/FlubberMorph.tsx
import { useEffect, useRef, useState } from 'react'
import { interpolate } from 'flubber'
export function FlubberMorph() {
const pathRef = useRef<SVGPathElement>(null)
const animFrameRef = useRef<number | null>(null)
const morphBetween = (
startPath: string,
endPath: string,
durationMs = 800
) => {
const interpolator = interpolate(startPath, endPath, { maxSegmentLength: 2 })
const start = performance.now()
const animate = (now: number) => {
const elapsed = now - start
const t = Math.min(elapsed / durationMs, 1)
const ease = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t
if (pathRef.current) {
pathRef.current.setAttribute('d', interpolator(ease))
}
if (t < 1) {
animFrameRef.current = requestAnimationFrame(animate)
}
}
if (animFrameRef.current) cancelAnimationFrame(animFrameRef.current)
animFrameRef.current = requestAnimationFrame(animate)
}
const squarePath = 'M 20,20 L 80,20 L 80,80 L 20,80 Z'
const blobPath = 'M 50,10 C 80,10 90,30 90,50 C 90,70 70,90 50,90 C 30,90 10,70 10,50 C 10,30 20,10 50,10 Z'
const [isBlob, setIsBlob] = useState(false)
useEffect(() => {
if (pathRef.current) {
pathRef.current.setAttribute('d', squarePath)
}
}, [])
const toggle = () => {
morphBetween(
isBlob ? blobPath : squarePath,
isBlob ? squarePath : blobPath
)
setIsBlob(!isBlob)
}
return (
<div>
<svg viewBox="0 0 100 100" className="w-48 h-48">
<path ref={pathRef} fill="#8b5cf6" />
</svg>
<button onClick={toggle} className="mt-4 px-4 py-2 bg-purple-500 text-white rounded">
Morph
</button>
</div>
)
}
If both shapes have the same number of points, you can use SMIL without any libraries. Example: square morphs to diamond and back. See MDN documentation.
export function SMILMorph() {
return (
<svg viewBox="0 0 100 100" className="w-32 h-32">
<path fill="#f59e0b">
<animate
attributeName="d"
dur="1.5s"
repeatCount="indefinite"
values="
M 20,20 L 80,20 L 80,80 L 20,80 Z;
M 50,10 L 90,50 L 50,90 L 10,50 Z;
M 20,20 L 80,20 L 80,80 L 20,80 Z
"
keyTimes="0; 0.5; 1"
calcMode="spline"
keySplines="0.4 0 0.2 1; 0.4 0 0.2 1"
/>
</path>
</svg>
)
}
How to Prepare SVG Paths for Morphing?
For successful morphing, shapes must be optimized. Use tools:
- SVGO — minimize and normalize paths.
- Inkscape → Extensions > Modify Path > Add Nodes — align point count.
-
GSAP MorphSVGPlugin.convertToPath() — converts
<circle>,<rect>, etc. into<path>.
import { MorphSVGPlugin } from 'gsap/MorphSVGPlugin'
MorphSVGPlugin.convertToPath('#my-circle, #my-rect')
Additional preparation tips
Before morphing, check the number of nodes with `path.getTotalLength()`. The difference should be minimal. For complex shapes, use GSAP — it aligns nodes automatically. If budget is tight, choose Flubber.js and prepare paths manually.Process and Timelines
- Analysis — agree on design and verify source shapes.
- Optimization — clean paths with SVGO, align nodes.
- Library selection — recommend GSAP for complex projects.
- Implementation — write animation, test on mobile devices.
- Integration — embed into application, check Core Web Vitals (LCP, CLS).
| Work Type | Timeline |
|---|---|
| Simple two-state morphing | from 4 hours |
| Interactive morphing with shape selection | 1–2 days |
| Complex sequence with color and interactivity | up to 5 days |
The cost includes documentation, repository access with demo, training for the client's team, and 30-day support. We guarantee smooth 60 FPS on all devices. Order morphing for your website — get a consultation and project estimate within 1 day. Contact us.
Typical Mistakes
- Mismatch in node count — check with
path.getTotalLength(). Even one extra node breaks the animation. - Ignoring ease functions — without them the animation looks unnatural. Use
power2.inOutorback.out. - No window resize handling — shapes may shift if coordinates aren't recalculated. Bind animation to resize via ResizeObserver.
We guarantee that with our experience (5+ years, 50+ projects) your morphing will run without stuttering. Contact us for a consultation.







