Deep Dive · June 2026 · 6 min read

Scroll-driven SVG
path morphing.

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

What is SVG path morphing?

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 down

Under the hood

How svg-scroll-draw morphs paths

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.

morphing (simplified)
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 d

This 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

Paths must be numerically compatible

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

The easiest way to ensure compatibility: draw both shapes in Figma or Illustrator, export as SVG, and verify the 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

When to reach for morphTo

Icon 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

Draw and morph simultaneously

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.

combined.js
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%' },
});
React version
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

What morphTo can't do

!

<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

Full API

OptionTypeNotes
morphTostringTarget path d attribute. Must be numerically compatible with the source path.
easingEasingName | fnControls the morph curve, not just the draw. spring and bounce give great shape transitions.
fadebooleanFade in simultaneously with draw + morph.
oncebooleanStay morphed at the target shape after first completion.
trigger{ start, end }Viewport anchors for when the morph starts and ends.
strokeColorstring | [string, string]Interpolate stroke color as the shape morphs.
onComplete() => voidFires 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.