Morphing icons — the constraint-driven approach
Constraint-driven icon system — every icon = N SVG lines, unused lines collapse to invisible center points, same-shape icons rotate.
Raw markdown On GitHubreferences/motion/morphing-icons.md
Taught in
Benji Taylor's icon system for the Claude app: every icon in the set is constructed from exactly three SVG lines. Icons with fewer visible elements collapse their unused lines to invisible center points. Icons sharing geometry differ only by rotation. The result: every icon can morph smoothly into any other icon in the set, because they all share the same data shape.
The 3-line constraint
type IconLine = { x1: number; y1: number; x2: number; y2: number; opacity?: number };
type Icon = [IconLine, IconLine, IconLine]; // always 3 lines
const arrowUp: Icon = [
{ x1: 7, y1: 12, x2: 7, y2: 2 }, // vertical shaft
{ x1: 2, y1: 7, x2: 7, y2: 2 }, // left wing
{ x1: 12, y1: 7, x2: 7, y2: 2 }, // right wing
];
const dotIcon: Icon = [
{ x1: 7, y1: 7, x2: 7, y2: 7, opacity: 0 }, // collapsed
{ x1: 7, y1: 7, x2: 7, y2: 7, opacity: 0 }, // collapsed
{ x1: 7, y1: 7, x2: 7, y2: 7, opacity: 1 }, // visible single point
];A collapsed line is { x1: 7, y1: 7, x2: 7, y2: 7 } with opacity: 0. Visually it's a point at center; arithmetically it's still a "line" the morph can tween between.
Two morph strategies
1. Coordinate interpolation (different shapes)
When morphing between two icons with different geometries, let Motion (or your animation library) interpolate each coordinate independently:
<motion.line
x1={current[0].x1} y1={current[0].y1}
x2={current[0].x2} y2={current[0].y2}
animate={{ x1: target[0].x1, y1: target[0].y1, x2: target[0].x2, y2: target[0].y2 }}
/>Motion handles the tweening. The animation is smooth because the count of lines is constant (3 → 3); only the coordinates change.
2. Rotation morph (shared geometry)
When two icons share the same line shape (e.g., all four directional arrows), rotate the whole icon instead of tweening coordinates:
<motion.svg
animate={{ rotate: rotationFor(currentDirection) }}
transition={{ type: 'spring', stiffness: 380, damping: 30 }}
>
{/* same SVG content for all four arrow directions */}
</motion.svg>Rotation is cheaper, faster, and reads as more obviously "the same object turning" instead of "two different objects swapping."
Use rotation when the geometric difference is purely angular (arrows by 90°, plus → cross by 45°). Use coordinate interpolation when the geometry actually changes (arrow → checkmark).
When to apply
- Toggle states: play/pause, expand/collapse, mute/unmute, follow/unfollow.
- Directional icons: chevron up/down/left/right.
- Status indicators that shift meaning: dot → ring → check.
- Toolbar icons that change mode.
When NOT to apply
- Marketing/decorative icons where personality and detail matter more than morphability. Use a real icon pack like Phosphor or Hugeicons (icon-systems).
- Icons that need filled shapes (the constraint here is stroke-only).
- Icon sets larger than ~20 icons. The 3-line constraint gets restrictive past that.
Constraints are the engine: forcing every icon into one 3-line schema is what makes every icon morph-compatible with every other.
When the icons can't share a schema: the contextual swap
For a stock icon pair (copy → check, like → liked), swap them with a deep scale, not a plain fade: scale 0.3 → 1, opacity 0 → 1, blur(3px) → 0, spring { type: "spring", duration: 0.35, bounce: 0 }, inside AnimatePresence mode="popLayout" initial={false}. A timid 0.8–0.9 scale reads as a glitch. Without Motion, render both icons stacked and cross-fade classes over ~300ms with cubic-bezier(0.25, 0.1, 0, 1). Keep it for state toggles and contextual toolbars, never always-on navigation icons.
Gotcha
3 lines is right for the Claude app icon vocabulary. Your product may need 4 (more complex icons) or 2 (simpler set). Pick the smallest N that covers your icon space. Larger N means more "collapsed" lines per icon, which is fine, but eventually you've defeated the purpose — pick judiciously.
Also: opacity-collapsed lines still render in the SVG. If you have hundreds of icons on a page, the DOM weight adds up. Use this pattern for ~5–20 icons in active use, not for an icon library.
Sources
- Benji Taylor — Morphing Icons with Claude, benji.org/morphing-icons-with-claude.
- Emil Kowalski's design-engineering practice, distilled by HKTITAN (contextual icon swap values).
- Related: icon-systems, transform-mastery, fly-not-teleport, animations-dev-curriculum.
motioniconssvgbenji