Snap.svg Morphing Icon Button — Path-to-Path Icon Animation Snippet
Snap.svg Morphing Icon Button · Buttons · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="smi-stage">
<div class="smi-head">
<span class="smi-tag">Snap.svg · path morphing</span>
<h2>Morphing Icon Button</h2>
<p>Click the button — the hamburger path morphs directly into a checkmark.</p>
</div>
<button class="smi-btn" id="smiBtn" aria-label="Toggle menu">
<svg id="smiSvg" width="34" height="34" viewBox="0 0 34 34">
<path id="smiPath" d="M6,9 L28,9 M6,17 L28,17 M6,25 L28,25" stroke="#fff" stroke-width="3" stroke-linecap="round" fill="none"></path>
</svg>
</button>
<p class="smi-state" id="smiState">Menu closed</p>
</div>*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:radial-gradient(120% 100% at 50% 0%,#161d38,#080a14);color:#fff;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.smi-stage{display:flex;flex-direction:column;align-items:center;gap:20px}
.smi-head{text-align:center;max-width:360px}
.smi-tag{display:inline-block;font-size:11px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:#fbbf24;background:rgba(251,191,36,.12);border:1px solid rgba(251,191,36,.3);padding:5px 12px;border-radius:99px;margin-bottom:12px}
.smi-head h2{font-size:clamp(24px,5vw,32px);font-weight:800;letter-spacing:-.02em}
.smi-head p{font-size:13.5px;color:#8e97b8;margin-top:7px}
.smi-btn{width:76px;height:76px;border-radius:50%;border:1px solid rgba(255,255,255,.12);background:#1b1f3a;display:flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:0 18px 40px -18px rgba(0,0,0,.7);transition:background .2s}
.smi-btn:hover{background:#242a4e}
#smiPath{transition:stroke .42s ease-in-out}
.smi-state{font-size:12.5px;color:#9aa3c4;font-weight:600}var s = Snap('#smiSvg');
var path = s.select('#smiPath');
var HAMBURGER = 'M6,9 L28,9 M6,17 L28,17 M6,25 L28,25';
// Split into 3 M/L segments (6 points total) to structurally match the
// hamburger path above -- the midpoint 10,21 is collinear with the real
// checkmark stroke, so visually it is still a single clean diagonal.
var CHECKMARK = 'M6,17 L10,21 M10,21 L14,25 M14,25 L28,9';
var isCheck = false;
document.getElementById('smiBtn').addEventListener('click', function () {
isCheck = !isCheck;
var target = isCheck ? CHECKMARK : HAMBURGER;
var color = isCheck ? '#34d399' : '#ffffff';
// Snap's path.animate morphs by interpolating each point of the CURRENT
// path toward the corresponding point of the target path string, in
// order. It works cleanly here because both strings describe three
// 2-point line segments \u2014 same command count, same point count.
path.animate({ d: target }, 420, mina.easeinout);
// Stroke color fades via the CSS transition on #smiPath. Animating stroke through
// Snap too makes it read the computed rgb() value back as a selector and throw.
path.attr({ stroke: color });
document.getElementById('smiState').textContent = isCheck ? 'Menu closed (checkmark)' : 'Menu closed';
});Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.
What's included
Features
About this UI Snippet
Snap.svg Morphing Icon Button — How Path Morphing Actually Works
Most "icon morph" buttons on the web fake it: two separate icons cross-fade, or a hamburger's three lines individually rotate and translate into an X via CSS transforms. This snippet does something more literal — the underlying d attribute of a single <path> element morphs directly from one shape's coordinates into another's, using Snap.svg's path.animate({ d: newPath }, ...).
What .animate({ d: ... }) actually interpolates
SVG path data is a sequence of commands (M, L, C, etc.) each carrying coordinate points. When you animate the d attribute, Snap.svg does not understand the *shapes* geometrically — it walks the current path's list of points and the target path's list of points, in order, and linearly interpolates each corresponding pair over the animation's duration. Point 1 of the start path glides toward point 1 of the end path, point 2 toward point 2, and so on.
Why this snippet's two paths morph cleanly
HAMBURGER = 'M6,9 L28,9 M6,17 L28,17 M6,25 L28,25' — three separate 2-point line segments (M start point, L end point), six points total.
A checkmark is naturally just two strokes — one short diagonal down, one long diagonal up — which would be only four points, structurally mismatched with the hamburger's six. This snippet's CHECKMARK string deliberately splits the first, shorter stroke into two collinear segments (M6,17 L10,21 M10,21 L14,25) purely to pad the point count back up to six, then adds the second stroke (M14,25 L28,9) — three M/L pairs, six points, matching the hamburger exactly. The extra midpoint at 10,21 sits exactly on the line between 6,17 and 14,25, so it changes nothing about how the checkmark *looks*; it only exists to give the morph a point to interpolate toward. Because both strings have the same number of commands in the same order with the same number of coordinate pairs, each point maps onto a sensible corresponding point on the other shape, and the interpolation reads as one coherent shape smoothly folding into another.
What happens when path structures don't match
If the two path strings had a different number of points or a different sequence of command types (say, one uses M/L/L and the other M/C/C with curves), Snap.svg's simple linear point interpolation has no semantic understanding of "this line corresponds to that curve." It still animates *something* — but the correspondence becomes arbitrary point-index pairing, and the mid-animation frames often look like the path is being dragged inside out or crossing over itself in an ugly, unpredictable way. Clean morphs are a path-authoring discipline, not an automatic feature: you get a good morph by deliberately constructing both path strings with matching command counts and orders, often by adding "dummy" points to the simpler shape so it structurally mirrors the more complex one.
The rest of the interaction
stroke is switched in the same click handler and eased by a CSS transition on the path, so the icon's color fades from white to green in step with the shape change (it is deliberately not animated through Snap, which reads the current rgb() value back and fails on it), and mina.easeinout (Snap's built-in easing namespace) shapes the timing curve so the morph accelerates then decelerates rather than moving at constant speed.
Step by step
How to Use
- 1Add the Snap.svg CDNInclude snap.svg-min.js from the CDN panel — it attaches a global Snap function.
- 2Paste HTML, CSS, and JSA round icon button renders with a hamburger path.
- 3Click the buttonThe path morphs into a checkmark and the stroke eases to green.
- 4Click againIt morphs back to the hamburger, reversing color too.
- 5Swap in your own icon pairWrite two path strings with matching command counts and order.
- 6Adjust timingChange the 420ms duration or swap mina.easeinout for another mina easing.
Real-world uses
Common Use Cases
mina.easeinout supplying the timing curve without a separate easing library.Got questions?
Frequently Asked Questions
path.animate({ d: newPathString }, duration, easing) walks the current path\u2019s sequence of coordinate points and the target path\u2019s sequence of coordinate points, in order, and linearly interpolates each corresponding pair over the animation duration. It is point-index interpolation, not shape-aware.
Snap\u2019s interpolation maps point N of the start path to point N of the end path. A checkmark naturally has only two strokes (four points), so this snippet splits its shorter stroke into two collinear segments purely to pad it to three M/L pairs (six points), matching the hamburger. Matching command counts and orders means each point has a sensible counterpart on the other shape, so the animation reads as one coherent shape folding into the other instead of an arbitrary point shuffle.
Snap.svg still animates, but the point correspondence becomes arbitrary \u2014 point 4 of a 6-point path might map onto a point that is not remotely near it visually on the target shape, and the path can look like it is being dragged inside out mid-animation instead of morphing cleanly.
Not cleanly with a direct Snap.animate({ d }) call \u2014 the command types themselves need to line up, or you need to convert both paths to the same command vocabulary (e.g. express straight lines as degenerate cubic curves) before animating, since Snap interpolates points within matching commands rather than translating between command types.
Snap.svg 0.5.1 reads the current stroke back as a computed rgb() string when animating it, and its color parser then passes that string to querySelector, which throws a Script error. Setting the stroke with path.attr and letting a CSS transition on the path fade it, over the same ~420ms, avoids the bug and stays in sync with the morph.
Author a third path string with the same three-M/L-segment structure (six points), track a small state enum instead of a boolean, and pass the appropriate target path/color to path.animate based on the current state on each click.