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
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.
Points · start → end
Preview · scrollDraw · scroll the box
- 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
<!-- 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>// 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).
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.
| Written | Becomes |
|---|---|
| M m | Starts a new piece. Pairs after it are lines, as the spec reads them. |
| L l · H h · V v | A straight cubic: its controls a third and two thirds of the way along, so it draws the same line. |
| C c | Kept, in absolute numbers. |
| S s | A cubic whose first control mirrors the one before it, or sits on the pen when the one before is not a cubic. |
| Q q | The cubic that draws the same curve: controls two thirds of the way from each end to the quadratic’s one. |
| T t | The same, with the control mirrored from the quadratic before. |
| A a | One 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 z | A 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.
getPointAtLength(): the two outlines agree to within a thousandth of a unit.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.
| Case | What scrollDraw does |
|---|---|
| How it moves | Each 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 differ | A 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. |
| Easing | The 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 line | The 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. |
| Elements | A <path> only. On a rect, circle, line or polygon morphTo is skipped, and a development build says so. |
| Native path | A morph always runs on the JavaScript loop: morphTo is one of the options that keep scrollDraw off the browser’s own scroll timeline. |
| once | Holds 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 motion | The 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). |
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.
| Engine | transition: d | @keyframes d | Scroll timeline | SMIL animate d |
|---|---|---|---|---|
| Chromium | Morphs | Morphs | Morphs | Morphs |
| Firefox | Morphs | Morphs | Morphs | Morphs |
| WebKit (Safari) | No d in CSS | No d in CSS | The timeline runs; d does not | Morphs |
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.
<!-- 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>/* 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; }
}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">.Use it in your project
SVG morph questions
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.
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.
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.
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.
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.
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.
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.