All components

Morph Icon

Icons

True SVG path interpolation between two icons — not a cross-fade. Ships its own path normalizer with no dependency: a cursor-based parser (correct on packed arc flags, where a number regex silently fails), every command reduced to cubics including the full endpoint→centre arc conversion, equal-arc-length resampling, and a rotation/direction search that minimises total travel so the morph never ties itself in a knot. Multiple subpaths are paired by centroid, and an unmatched one collapses into its nearest counterpart.

External motion value

Scrub slowly through the middle — no crossings, no knots.

Spring · both directions

Click repeatedly — it reverses cleanly from wherever it is.

3 subpaths → 2 · the middle line collapses into the X

Installation

Terminal
npx shadcn@latest add https://ui.saumyarex.xyz/r/morph-icon.json

Props

PropTypeDefaultDescription
from / tostringPath data for state 0 and state 1. Any SVG path grammar — absolute or relative, `H`/`V`/`S`/`Q`/`T`/`A` shorthands, packed arc flags, exponent notation. Multiple subpaths are supported and paired automatically; the counts do not need to match.
progressMotionValue<number> | numberExternal 0→1 driver, and it takes precedence over `active`. Passing a `MotionValue` keeps the entire morph off React's render path — scrubbing it re-renders nothing. A plain number is synced into one for you.
activebooleanfalseConvenience driver that springs 0↔1. Reverses cleanly from wherever it is mid-flight. Ignored when `progress` is set.
samplesnumber72Points sampled per subpath. Mid-flight a sharp corner is rounded by at most half the sample spacing — sub-pixel at icon size. At rest the authored `d` is returned verbatim, so the idle icon is never an approximation.
springSpringOptions{ stiffness: 300, damping: 30, mass: 0.6 }Spring for the `active` driver.
labelstringAccessible name. Omit and the icon is `aria-hidden` — the usual case, since it normally sits inside a labelled button.
...propsReact.ComponentProps<"svg">Forwarded to the `<svg>`. `viewBox` defaults to `0 0 24 24`, and it is stroked by default (`fill: none`, `stroke: currentColor`, width 2) — pass `fill="currentColor"` for filled icons.

Dependencies

motionclsxtailwind-merge