Deep Dive · June 2026 · 6 min read
The morphTooption lets you interpolate a SVG path's d attribute from its original shape to a target shape — all driven by scroll position. No GSAP MorphSVGPlugin. No paid add-ons. Zero extra dependencies.
Extra dependencies
0
vs GSAP MorphSVG
free
Works with
<path>
Triggers on
scroll
The concept
Every SVG <path> has a d attribute — a string of commands that defines its shape. Path morphing means smoothly interpolating between two d strings at a given progress value (0–1).
At progress 0, the path looks like the original shape. At progress 1, it looks like morphTo. Anywhere in between, each numeric coordinate is linearly interpolated — producing a smooth shape transition.
Combined with scroll-driven progress, you get shape changes that respond directly to how far the user has scrolled — without a single line of animation logic.
// A circle → square morph driven by scroll
scrollDraw('#shape', {
morphTo: 'M10 10 L90 10 L90 90 L10 90 Z', // target: a square
easing: 'ease-in-out',
trigger: { start: 'top 70%', end: 'top 20%' },
});
// The path starts as its original shape (e.g. a circle)
// and morphs into the square as the user scrolls downUnder the hood
The engine extracts all numeric tokens from both the original and target d strings, then linearly interpolates each token pair at the current scroll alpha. The non-numeric parts (command letters like M, L, C) are taken from the original path — they define the shape structure, which must be compatible between the two paths.
function morphPath(from: string, to: string, t: number): string {
const toNums = to.match(/[-+]?[d.]+/g).map(Number);
let idx = 0;
return from.replace(/[-+]?[d.]+/g, (match) => {
const fromNum = parseFloat(match);
const toNum = toNums[idx++] ?? fromNum;
return String(fromNum + (toNum - fromNum) * t);
});
}
// At t=0: returns original path d
// At t=0.5: returns midpoint between original and target
// At t=1: returns morphTo path dThis runs every animation frame while the element is in view, updating the d attribute directly on the <path> element. The stroke-dashoffset draw animation runs simultaneously — path draws and morphs at the same time.
The golden rule
The morph works by pairing numeric tokens from the original and target path strings one-for-one. If they have different counts, the extra target tokens are ignored (morphing snaps to a close approximation). For a perfect morph, both paths should have the same number of numeric tokens.
Compatible ✓
<!-- Both have 8 numeric tokens --> <path d="M10 10 L90 10 L90 90 L10 90" /> morphTo: "M50 10 L90 50 L50 90 L10 50"
Square → Diamond: same structure, same token count. Morphs cleanly.
Incompatible ✗
<!-- Triangle: 6 tokens --> <path d="M50 10 L90 90 L10 90 Z" /> <!-- Star: 20 tokens --> morphTo: "M50 5 L61 35 L95 35 L68 57 ..."
Different command counts — morph will be approximate or look wrong.
Tip: use the same SVG editor
d strings have the same number of path commands. Most shape transformations (square → diamond, circle → blob) work when you constrain yourself to the same command structure.Use cases
morphToIcon transitions
Morph a play button into a pause button, a hamburger into an X, or a circle into a checkmark as the user scrolls past a milestone.
scrollDraw('#icon', {
morphTo: pauseIcon,
easing: 'spring',
trigger: { start: 'top 60%', end: 'top 40%' },
});Data visualisation
Morph a bar chart shape into a line chart shape, or transform a circle chart into its expanded state as the section scrolls into view.
scrollDraw('#chart-shape', {
morphTo: lineChartPath,
easing: 'ease-in-out',
once: true,
});Blob / organic shapes
Animate background blobs or decorative shapes between two organic forms. Combine with fade for a smooth entrance.
scrollDraw('#blob', {
morphTo: blobVariant2,
fade: true,
easing: 'ease-out',
});Logo storytelling
Draw the logo as a simple shape, then morph it into its final form as the hero section exits the viewport.
scrollDraw('#logo-path', {
morphTo: finalLogoPath,
easing: 'spring',
trigger: { start: 'top 90%', end: 'top 10%' },
});Combining effects
morphTo runs alongside the stroke-dashoffset draw animation — both update on the same scroll alpha. The path traces itself in and transforms its shape at the same time. Combined with fade and strokeColor, this produces effects that feel far more complex than the code suggests.
import { scrollDraw } from 'svg-scroll-draw';
// Path draws in, morphs shape, changes colour, and fades — all at once
scrollDraw('#hero-shape', {
morphTo: finalShape,
strokeColor: ['#ff90e8', '#5865F2'], // pink → indigo as it draws
fade: true, // opacity 0 → 1
easing: 'ease-in-out',
once: true,
trigger: { start: 'top 80%', end: 'top 20%' },
});import { ScrollDraw } from 'svg-scroll-draw/react';
function HeroShape() {
return (
<ScrollDraw
morphTo={finalShape}
strokeColor={['#ff90e8', '#5865F2']}
fade
easing="ease-in-out"
once
trigger={{ start: 'top 80%', end: 'top 20%' }}
>
<svg viewBox="0 0 100 100">
<path d={originalShape} stroke="currentColor" strokeWidth="2" fill="none" />
</svg>
</ScrollDraw>
);
}Limitations
<path> only
morphTo silently no-ops on <rect>, <circle>, <line>, <polyline>, and other SVG shape elements. Convert them to <path> equivalents first.
Non-SVG elements
This is SVG path interpolation — it does not work on HTML elements, CSS clip-paths, or canvas. For HTML morphing you still need GSAP or the Web Animations API.
Complex structure changes
If your paths have fundamentally different command structures (e.g. one uses cubic beziers, the other straight lines), the interpolation will look wrong. Restructure to match.
Activates native CSS opt-out
morphTo requires JS per-frame updates, so it automatically bypasses the native CSS animation-timeline path. That's fine — the JS engine handles it seamlessly.
Quick reference
| Option | Type | Notes |
|---|---|---|
| morphTo | string | Target path d attribute. Must be numerically compatible with the source path. |
| easing | EasingName | fn | Controls the morph curve, not just the draw. spring and bounce give great shape transitions. |
| fade | boolean | Fade in simultaneously with draw + morph. |
| once | boolean | Stay morphed at the target shape after first completion. |
| trigger | { start, end } | Viewport anchors for when the morph starts and ends. |
| strokeColor | string | [string, string] | Interpolate stroke color as the shape morphs. |
| onComplete | () => void | Fires when morph reaches 100%. Swap to a static version here if needed. |
Try it in the Playground
Paste any two compatible SVG paths and set morphTo to see it live.