SVG Path Morph
A vector SVG path, filled with a violet-pink-cyan gradient, that reshapes on every click from star to heart to circle in 0.8 s: all three shapes are drawn with the same sequence of commands (a start, ten curves, a close), so every point glides to its match. Sharp at any size, a real button for the keyboard, the shape's name is announced; the shape is set without morphing when the user reduces motion.
Updated
This effect is part of Effect.Labs — 840 vanilla effects, some free, some premium. Liquid has 31 effects, including 6 free. Explore the category →
3 usage examples



How it works
The .svg-morph-btn button holds a 150 px <svg> (viewBox="0 0 100 100"), a diagonal linear gradient and a <path> whose d attribute is animated by a SMIL element: <animate attributeName="d" dur="0.8s" fill="freeze" begin="indefinite">. On click, the script sets from to the shape on screen, to to the next one, then calls beginElement(): the browser glides every number of the path from one to the other, at constant speed, and fill="freeze" holds the end shape. The .svg-morph-label text takes the next name from data-names ("Star,Heart,Circle").
For the interpolation to exist, the three paths have exactly the same structure: one M, ten C curves and a Z, starting at the top and going the same way around. The star's straight sides are curves whose control points sit on the segment, at one third and two thirds; the heart is cut into ten curve pieces, its landmarks lined up with the star's (the top dip on the top tip, the bottom tip on the bottom notch); the circle is made of ten 36° arcs. Two paths with different structures, straight lines on one side and curves on the other, do not interpolate: the shape jumps halfway.
The script remembers the shape on screen (idx) instead of reading it back from the d attribute: the SMIL animation does not change that attribute, it covers it; reading it would restart every click from the shape loaded with the page. The d attribute is still updated on each click, so a browser without SMIL shows the right shape. During the 0.8 s morph, clicks are ignored: the in-between value of a SMIL animation cannot be read, and restarting from a finished shape would make the path jump.
Accessibility
- Keyboard and screen readers: the SVG sits in a real
<button>named "Next shape" (the SVG itself is hidden witharia-hidden). Measured in Chromium 151 and Firefox 153: Tab reaches it, Enter and Space move to the next shape. The label hasaria-live="polite": "Heart", "Circle" are announced. In Safari, Tab only stops on buttons if the keyboard setting asks for it (or with Option+Tab). - Reduced motion: under
prefers-reduced-motion: reduce, the script givesfromandtothe same value: the new shape appears at once. Measured in all three engines: no in-between frame. - The shape must not be the only information: the label says in text what the shape means ("Heart: 5th purchase", "I love it"), as in the examples.
- Focus ring: 2 px pink, 4 px offset around the button, visible for keyboard users only (
:focus-visible).
Browser compatibility
SVG + SMIL (<animate> on the d attribute) + 2.7 KB of JavaScript, mostly the text of the three paths; no dependencies. Measured in real time in Chromium 151, Firefox 153 and WebKit 26: every click starts from the shape on screen, the path changes on every frame for 0.8 s, with no jump. SMIL animation of d works in all current browsers. The CSS d: path() property, which would allow a CSS transition, only exists in Chromium and Firefox (missing from WebKit, measured): that is why the code goes through SMIL.
Without JavaScript, the star stays on screen. Without SMIL, the script still writes the new shape into the d attribute: it switches at once instead of morphing.
The code
HTML structure to paste into your page (the CSS + JS come with your purchase):
<div class="fx-0407-root">
<div class="svg-morph-container">
<button type="button" class="svg-morph-btn" aria-label="Next shape" data-names="Star,Heart,Circle">
<svg width="150" height="150" viewBox="0 0 100 100" aria-hidden="true" focusable="false">
<defs><linearGradient id="fx0407-grad">… 3 colors</linearGradient></defs>
<path fill="url(#fx0407-grad)" d="M50 5 C… (10 curves) Z">
<animate attributeName="d" dur="0.8s" fill="freeze" begin="indefinite"></animate>
</path>
</svg>
</button>
<span class="svg-morph-label" aria-live="polite">Star</span>
</div>
</div>
One-time payment, no subscription — the code is yours forever
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Duration (HTML — dur on <animate>) |
0.8s | The script reads this value to know how long to ignore clicks: change it in one place only. Beyond 1.2 s, the button feels slow. |
Easing (HTML — calcMode, keyTimes, keySplines on <animate>) |
constant speed | calcMode="spline" keyTimes="0;1" keySplines=".45 0 .2 1" makes the morph start and end smoothly. |
Colors (HTML — the gradient's three <stop> elements) |
violet #a855f7, pink #ec4899, cyan #06b6d4 |
Also change the gradient's id (fx0407-grad) and the path's url(#…) if you put several copies of the effect on the page: two gradients with the same id get mixed up. |
Size (HTML — width / height of the <svg>) |
150 × 150 px | The path is vector: 48 px for an icon, 300 px for a visual, with no loss of sharpness. The viewBox stays at 0 0 100 100. |
Labels (HTML — the button's data-names) |
"Star,Heart,Circle" (English version) | Three comma-separated names, in shape order. The script rereads the attribute on every click. |
Shapes (JS — the shapes array) |
star, heart, circle | Any new shape needs the same structure: M, ten C, Z, starting at the top and going clockwise. Also put the first shape in the HTML d attribute. |
FAQ
M at the topmost point, then ten C curves going clockwise around, then Z. A straight side becomes a curve whose two control points lie on the segment (at one third and two thirds). Match the landmarks: the n-th curve of one shape should sit at roughly the same place around the loop as the n-th curve of the others, or the path twists during the morph.dur.d: path(…) property, which works in Chromium and Firefox but not in WebKit (measured: CSS.supports says no), so not in Safari. The <animate> element animates the d attribute in all three engines. The structure rule (same commands, same count) applies to both methods.