tool / svg morphruns in your browser

SVG morph generator

Pick two shapes, or paste two paths. The generator rewrites both as the same cubic curves, numbers the points so you can see which goes where, turns the end shape so it does not twist, and morphs it with scrollDraw as you scroll. Copy the morphTo code, a React component, a CSS version or the pair.

Shapes
13
Commands read
10
Code
4
Library
2.23.3
135791113151719start · circled = M A A Z→ M C×20 Zend · stard = M L×9 Z→ M C×20 Zt = 0.5each point i, each step t:x = x₀ + (x₁ − x₀) · ty = y₀ + (y₁ − y₀) · t20 points, 122 numbers
Fig. 1 — the circle and star this page opens with, each rewritten as 20 cubics. A thin line joins every point to its partner; morphTo moves each one along its line as you scroll. Dashed: the frame half way.
01The tool

Morph one shape into another

On the left, both shapes as the generator matched them: the start shape solid, the end shape dashed, each point numbered and joined to its partner. Point 1 is where the end shape starts; move it with the matching controls. On the right, scrollDraw itself, in a small box that scrolls: the drawing sticks inside a section four boxes tall, as the generated code has it on a page.

Start shape

d = M A A Z · 4 segments as cubics

End shape

d = M L×9 Z · 10 segments as cubics

Points · start → end

135791113151719

Preview · scrollDraw · scroll the box

Matching
End shape’s start point · 6 of 20
Direction of the end shape
Least travel
Points per piece · 20 (auto)
Decimals
Animation
Outline
Fill
Start
d = M A A Z
End
d = M L×9 Z
Both now
d = M C×20 Z · 122 numbers each
Travel
23.6 units per point on average, the least
Length
start 477.5 · end 580.4
index.html36 lines
<!-- Made with https://svg-scroll-draw.vercel.app/svg-morph-generator -->
<section id="morph" class="morph">
  <svg class="morph-svg" viewBox="0 0 200 200" aria-hidden="true">
    <path
      d="M176 100 C176 108.4 174.6 116.5 172.1 124 C169.6 131.6 165.9 138.6 161.3 144.9 C156.7 151.2 151.2 156.7 144.9 161.3 C138.6 165.9 131.6 169.6 124 172.1 C116.5 174.6 108.4 176 100 176 C91.6 176 83.5 174.6 76 172.1 C68.4 169.6 61.4 165.9 55.1 161.3 C48.8 156.7 43.3 151.2 38.7 144.9 C34.1 138.6 30.4 131.6 27.9 124 C25.4 116.5 24 108.4 24 100 C24 91.6 25.4 83.5 27.9 76 C30.4 68.4 34.1 61.4 38.7 55.1 C43.3 48.8 48.8 43.3 55.1 38.7 C61.4 34.1 68.4 30.4 76 27.9 C83.5 25.4 91.6 24 100 24 C108.4 24 116.5 25.4 124 27.9 C131.6 30.4 138.6 34.1 144.9 38.7 C151.2 43.3 156.7 48.8 161.3 55.1 C165.9 61.4 169.6 68.4 172.1 76 C174.6 83.5 176 91.6 176 100 Z"
      fill="none"
      stroke="currentColor"
      stroke-width="4"
      stroke-linecap="round"
      stroke-linejoin="round"
    />
  </svg>
</section>

<style>
/* The section is three screens tall and the drawing sticks inside it,
   so the shape changes while the reader scrolls through. */
.morph {
  height: 300vh;
}
.morph-svg {
  position: sticky;
  top: 0;
  display: block;
  width: 100%;
  height: 100vh;
  padding: 12vh 24px;
  box-sizing: border-box;
}

/* scrollDraw draws the outline as it morphs. This keeps it whole,
   so only the shape changes. */
.morph path {
  stroke-dasharray: none !important;
}
</style>
main.js12 lines
// Made with https://svg-scroll-draw.vercel.app/svg-morph-generator
import { scrollDraw } from 'svg-scroll-draw';

// Both paths are written M C×20 Z, 122 numbers each, so morphTo
// can move every number of the start shape to its partner.
scrollDraw('#morph', {
  morphTo: 'M155.2 98.6 C147.5 104.6 139.9 110.5 132.3 116.5 C135 125.8 137.6 135.1 140.3 144.4 C142.9 153.7 145.6 163 148.2 172.3 C140.2 166.9 132.1 161.5 124.1 156.2 C116.1 150.8 108 145.4 100 140 C92 145.4 83.9 150.8 75.9 156.2 C67.9 161.5 59.8 166.9 51.8 172.3 C54.5 163 57.1 153.7 59.8 144.4 C62.4 135.1 65.1 125.8 67.7 116.5 C60.1 110.5 52.5 104.6 44.9 98.6 C37.2 92.6 29.6 86.7 22 80.7 C31.7 80.3 41.3 80 51 79.6 C60.7 79.2 70.3 78.9 80 78.5 C83.3 69.4 86.7 60.3 90 51.3 C93.3 42.2 96.7 33.1 100 24 C103.3 33.1 106.7 42.2 110 51.3 C113.3 60.3 116.7 69.4 120 78.5 C129.7 78.9 139.3 79.2 149 79.6 C158.7 80 168.3 80.3 178 80.7 C170.4 86.7 162.8 92.6 155.2 98.6 Z',
  // From the section's top at the top of the screen to its bottom at the
  // bottom: the whole time the drawing is stuck in view.
  trigger: { start: 'top top', end: 'bottom bottom' },
  easing: 'ease-in-out',
});

Only options that differ from scrollDraw’s defaults are written; the trigger is the sticky section’s (top top → bottom bottom).

Run it
02The matching

How two paths are made to match

A morph moves numbers, so the two d strings need the same letters in the same order and as many numbers. Two shapes drawn apart almost never have that. The generator gets there in three steps, none of which changes how either shape looks.

Everything as cubics. Each command, absolute or relative, implicit repeats and packed arc flags included (a5 5 0 0110 10 is two flags, then 10), becomes absolute cubic Béziers. A cubic can draw every command exactly except an arc, which it follows to within 0.027% of the radius per quarter turn: 0.021 units on the 76-unit circle here.

WrittenBecomes
M mStarts a new piece. Pairs after it are lines, as the spec reads them.
L l · H h · V vA straight cubic: its controls a third and two thirds of the way along, so it draws the same line.
C cKept, in absolute numbers.
S sA cubic whose first control mirrors the one before it, or sits on the pen when the one before is not a cubic.
Q qThe cubic that draws the same curve: controls two thirds of the way from each end to the quadratic’s one.
T tThe same, with the control mirrored from the quadratic before.
A aOne cubic for each quarter turn or less, controls 4/3 · tan(θ/4) along the tangents. Radii too small to reach are scaled up, a zero radius is a line, as the spec says.
Z zA straight cubic back to the piece’s start when the pen is elsewhere; nothing when it is there already.

As many segments. Segments are cut with de Casteljau’s construction, which leaves the outline where it was, until each pair of pieces has the same count. Auto picks the fewest that keep the points evenly spaced on both: the square’s 4 sides and the triangle’s 3 meet at 12, 3 segments to each side of the square and 4 to each of the triangle’s, rather than at 4 with one triangle side cut in two. Pieces (each M) pair in order; when one shape has more, the extra ones shrink to the nearest point of the other shape’s outline.

The least travel. A closed outline can start anywhere along it and run either way round. The generator tries every start point of the end shape, both ways, and keeps the one whose points travel least: that is what stops a morph twisting, or turning inside out half way. The travel readout says how far, on average, and the controls let you pick another.

Checked
Every preset here, rewritten and cut into 24 segments, was drawn over the original in Chromium and both were sampled with getPointAtLength(): the two outlines agree to within a thousandth of a unit.
039 rules

What morphTo does with the pair

morphTo is an option of scrollDraw, the same call that draws the line, so the morph and the drawing share one progress. This is what it does, read from the library’s source and checked in Chromium, Firefox and WebKit. For when to reach for it, the post on scroll path morphing has the use cases.

CaseWhat scrollDraw does
How it movesEach frame, every number in the path’s d, as it was when scrollDraw started, is moved toward the number at the same place in morphTo by the progress, and kept to 4 decimals. The letters always come from the start shape; nothing is converted or matched for you.
When they differA development build warns once, at the start: the command sequence differs, or the coordinate count does. Extra numbers in morphTo are ignored and missing ones keep their start value, so the shape never arrives.
EasingThe morph follows the same eased progress as the line. spring rises to 1.36 and elastic to 1.27, so with those the shape goes past the end shape and back. seek() clamps to 0–1, which is why the preview scrolls instead.
The lineThe dash is measured once, from the start shape, with getTotalLength(). A longer end shape is left part drawn at the end: the star above stops at 82% of its outline in Chromium, Firefox and WebKit. A pathLength just under the start shape’s length keeps the dash in step; stroke-dasharray: none !important keeps the line whole.
ElementsA <path> only. On a rect, circle, line or polygon morphTo is skipped, and a development build says so.
Native pathA morph always runs on the JavaScript loop: morphTo is one of the options that keep scrollDraw off the browser’s own scroll timeline.
onceHolds the furthest the scroll has been. In 2.23.2 and earlier it held the eased value, so spring or elastic stopped for good on the overshoot, a shape past the end shape; 2.23.3 holds the scroll progress and eases after.
Reduced motionThe path is given morphTo at once, drawn whole: the end shape, no animation.
destroy()Leaves the path as it last was, mid-morph. A new instance on the same element reads that frame as its start shape, so set d back first (the React wrapper rebuilds when an option changes).
Fig. 1
The 20 lines in the figure are those straight paths: the circle’s points, evenly spaced, each going to a tip, an inner corner or the middle of an edge of the star.
04Without JavaScript

Morphing with CSS instead

CSS can morph a path too, as the d property: two path() values with the same commands interpolate number by number, the way morphTo does, so the same matched pair works. Two with different commands do not interpolate; the shape flips half way. The CSS only tab writes the scroll version, on a view timeline. Here is what each engine did with it, tested in Playwright’s Chromium, Firefox and WebKit.

Enginetransition: d@keyframes dScroll timelineSMIL animate d
ChromiumMorphsMorphsMorphsMorphs
FirefoxMorphsMorphsMorphsMorphs
WebKit (Safari)No d in CSSNo d in CSSThe timeline runs; d does notMorphs

For a morph on a click rather than the scroll, a transition is enough: toggle the button’s aria-expanded and the CSS does the rest. The menu and close icons, matched by the generator: three lines become two, and the third shrinks onto a line of the cross.

index.html6 lines
<!-- Made with https://svg-scroll-draw.vercel.app/svg-morph-generator -->
<button class="menu-icon" aria-label="Menu" aria-expanded="false">
  <svg viewBox="0 0 200 200" width="32" height="32" aria-hidden="true">
    <path d="M40 64 C80 64 120 64 160 64 M40 100 C80 100 120 100 160 100 M40 136 C80 136 120 136 160 136" fill="none" stroke="currentColor" stroke-width="14" stroke-linecap="round" />
  </svg>
</button>
icon.css11 lines
/* Made with https://svg-scroll-draw.vercel.app/svg-morph-generator */
/* Chromium and Firefox. Safari keeps the menu icon. */
.menu-icon path {
  transition: d 0.3s ease;
}
.menu-icon[aria-expanded='true'] path {
  d: path('M52 52 C84 84 116 116 148 148 M52 148 C84 116 116 84 148 52 M118 118 C118 118 118 118 118 118');
}
@media (prefers-reduced-motion: reduce) {
  .menu-icon path { transition: none; }
}
Safari
Where d is not a CSS property, the CSS morph does nothing and the path keeps the shape in its attribute. scrollDraw sets the attribute itself, so its morph runs in Safari too, and so does SMIL’s <animate attributeName="d">.
05Next

Use it in your project

067 questions

SVG morph questions

Q1

How do I morph one SVG shape into another?

Write both shapes as paths with the same commands in the same order and the same count of numbers, then animate between them: scrollDraw’s morphTo as the page scrolls, CSS d: path() in Chromium and Firefox, or SMIL’s <animate attributeName="d"> in every browser. Each number moves in a straight line to its partner. The generator above rewrites any two paths into that form: every command as absolute cubics, segments split until both have as many, and the end shape’s start point chosen so the points travel least.

Q2

Why does my SVG morph twist or turn inside out?

Every point moves to the point at the same place in the other path. When the end path starts at a different place round the shape, points cross over and the shape twists; when one runs clockwise and the other anticlockwise, it turns inside out half way. Move the end shape’s start point and direction until the points travel least. The generator’s best fit tries every start point both ways round and keeps the one with the least travel, and its points view numbers each pair so you can see what goes where.

Q3

Do both SVG paths need the same number of points?

For a number-for-number morph, yes. morphTo pairs numbers by position: with a different count the extras are ignored and the missing ones keep their start value, so the morph never reaches the end shape, and a development build warns. In CSS, two path() values with different commands do not interpolate at all: in Chromium and Firefox the shape flips from one to the other half way through. Split segments of the shorter path, without changing its outline, until the counts match; the generator does that for you.

Q4

How do I morph an SVG on scroll?

With svg-scroll-draw, put the start shape in the path’s d and the end shape in morphTo: scrollDraw('#morph', { morphTo: '…' }). By default it morphs while the drawing crosses the screen. For a drawing that stays in view while it changes, make the section taller than the screen, stick the SVG inside it, and set trigger: { start: 'top top', end: 'bottom bottom' }, which is what the generator writes. Without JavaScript, a view-timeline with animation-range: contain 0% contain 100% gives the same window in Chromium and Firefox.

Q5

Can CSS animate the SVG path d attribute?

As the CSS d property, in Chromium and Firefox: d: path('…') can be transitioned, put in @keyframes and driven by a scroll timeline, and linear() easing past 1 carries the shape past its end. Safari has no d property in CSS, so there the path keeps its d attribute and nothing morphs. SMIL’s <animate attributeName="d"> morphs in all three, on a clock or a click rather than the scroll.

Q6

Why does the line not finish drawing when the shape morphs?

scrollDraw measures the dash once, from the start shape. When the end shape is longer, the dash is too short for it, and at the end the rest of the outline is left undrawn: from the circle to the star here, 82% of the star is drawn. Give the path a pathLength just under the start shape’s length, which the generator writes when “Keep the dash in step” is on: the browser then scales the dash to whatever length the outline has. Or keep the outline whole with stroke-dasharray: none !important and let only the shape change.

Q7

Does morphTo work on a circle or a rect?

No: it only changes a <path>, and on any other element it is skipped (a development build warns). Convert the shape to a path first; the SVG shape to path converter writes a circle as two arcs, which this generator turns into four cubics.

Morphing with svg-scroll-draw 2.23.3: morphTo in the docs, the SVG shape to path converter for shapes that are not paths yet, and the SVG path viewer to check a path before it morphs.