Every library in this category will morph any two icons you hand it. None of them will tell you when the result is worth shipping.
By the end of this page you will be able to refuse a bad pairing without looking at it, from two numbers a build step can produce. You will also have a figure for how rare a good arbitrary pair is, and it is lower than almost anyone guesses.
Here are twelve pairs, all compiled by the same pipeline, all rendering real geometry. Play them, and mark each one ship or refuse. Then turn on Show what the compiler decided and find out how many of your calls two numbers reached on their own.
Six of them are refused by default. Wherever your call matched, that was two numbers agreeing with your eye, and the rest of this page is about which two and why it took a while to find them.
300 pairs, spread deterministically across all 2003
Lucide icons so the sample includes genuinely unrelated things like chess-bishop and
person-standing. 300 of them imported and compiled, and every one that
did stayed faithful to its own reference curve, worst deviation 0.25
units on a 24 grid.
So the pipeline works on arbitrary input. That was never really in doubt.
What the same 300 also produced: 51% had a pairing the fitter itself called a coin toss, and 80% parked at least one slot, which means strokes appearing out of nothing rather than travelling. Only 3 came out clean on every axis.
scripts/presweep.ts, so this is 300 real icons rather than an illustration.That is the actual state of automatic icon morphing, and it is not a criticism of any particular implementation. between two unrelated drawings is often just undefined, and no solver recovers meaning that was never there.
The importer already reports a confidence per slot: how much better the chosen slot scored than the runner-up. Gating on it looks like the whole answer, so that is what I reached for first.
With Show what the compiler decided on, a second switch appears reading Show confidence instead. Turn that on and watch what happens to the ordering.
menu to x, the pair this entire library was built around, scores
0.000. So does arrow-right to arrow-down.
Meanwhile circle to guitar scores 0.399,
higher than most of the pairs you would actually ship.
The reason is worth sitting with, because it took a wrong turn to see. Confidence measures
ambiguity, and an icon that is symmetric produces ambiguity by construction. x is two
diagonals with the same and the same length, so nothing distinguishes them except
orientation and the fitter says so honestly. But when two slots fit equally well, either
choice usually looks fine. Ambiguity is close to the opposite of badness.
A gate built on confidence would have blocked this library's own headline morph and waved through a circle turning into a guitar.
. After the aligner solves for the best rotation and scale, whatever is left
over is residual. Zero means the two shapes really are one shape moved, which is exactly
when a morph reads as an object turning rather than as one drawing dissolving into another.
chevron-down to chevron-up is a rotation and scores
0.000. circle to guitar scores
0.846.
Park ratio. How many slots have no partner, and the smaller of the two nets: it refuses
27 pairs on its own where residual refuses
197. It catches a different kind of failure rather than more of
the same one. This is what "one icon is much more complex than the other" becomes once you
measure it: a three-stroke hamburger meeting a twelve-stroke
icon means nine strokes have to be born from nowhere. sun to moon scores fine on shape,
at 0.073, and still parks
8 of its 9 slots.
Refuse anything above 0.2 residual or 50% parked, and across the random 300 it admits 7.7%. Against pairs a developer would plausibly write, it admits 13 of 19. Against deliberately absurd ones, 0 of 8.
The six real pairs it stops are the right six, and two of them are checkable against decisions this project had already made by hand, months before the gate existed.
copy to check drives the copy button on every code block here. Getting it to look right
took a pinned slot assignment and a tick authored as two subpaths rather than one, both
recorded in scripts/precompile.ts with a comment explaining why. The gate flags it at
0.693 residual without being told any of that.
sun to moon was going to be the theme toggle, and it never became a morph. The decision
was to swap two Lucide icons instead, and CLAUDE.md records the reason: a sun with eight
rays has nothing in common with a crescent, so forcing it would have been a worse advert than
using two icons. The site ended up dark with no toggle at all, so the control is gone and the
judgement still stands. The gate reaches it from a park ratio of
89%.
A geometric threshold reproducing two human judgement calls, independently, is the strongest evidence available that it measures something real.
Four answers, and picking between them is the part a compiler cannot do for you.
Ship it anyway, because you looked and you disagree. The numbers describe geometry, not
taste, and play to pause is a genuinely non-rigid morph that plenty of products use
happily.
Fix the correspondence, which is the option a runtime solver cannot offer at all. When the pairing is wrong rather than the pair, you edit the and commit the fix, covered in fixing correspondence.
Or use two icons and cross-fade, which for a sun and a crescent is usually the honest answer.
Or draw, rather than morph. Animate a line being drawn along a route you author by hand, and the correspondence problem does not arise, because there is nothing to pair up.
The catch is that you are now the one doing the work. Nothing here can find that route for you. It is the right answer once the residual has told you the pair is not a morph, and the wrong one whenever a morph was available.
Redraw the pair as one stroked path that passes through both readings and animate
stroke-dashoffset. A dash offset is a single number with nothing to pair against
anything, which is why the problem disappears rather than being solved.
A hamburger uncurling into a cross is the usual demonstration. A strike-through arriving over a bell is the same trick with a mask carrying an offset copy of the line, so it reads as sitting above the icon rather than crossing it out.
The twelve pairs on this page are measured by scripts/pregate.ts during the build, using
the same two functions the rest of the site runs on: importSet, which turns raw icon
paths into a rig, and buildAlignment, which works out the pairing and reports how well it
fits. Both live in lib/morph. Change the candidate list, run pnpm precompile, and the
page changes with it.
The 300-pair sweep is the same code over a wider sample, run by
scripts/presweep.ts in the same build, against the same two thresholds. Both read them from
one file, so the twelve pairs above and the sweep cannot end up describing different gates.
Nothing here needs the site to run.
Residual says whether the two shapes were ever close enough to lie over each other, and parked ratio says how much of the icon is standing still while the rest moves. Either one over its threshold is a pairing to refuse. Both are cheap, both come out of a build rather than out of a reviewer's judgement, and a gate that runs there tells you before a reader finds out instead of after.
That is the last of the theory. Part 9 is the order you would build it in.
Two numbers that tell a good morph from a bad one before a user sees it.
You will need it in Part 9.