Skip to main content

Morphing Icons

Icons that transform through actual shape transformation, not crossfades. Inspired by Benji’s experiments with Claude.

Core Concept

Every icon uses exactly three SVG lines. Icons that need fewer lines collapse the extras to invisible points. This means any icon can morph into any other since they share the same underlying structure.

Three-Line Structure

All icons are defined with three lines, each with coordinates and optional opacity:
interface IconLine {
  x1: number;
  y1: number;
  x2: number;
  y2: number;
  opacity?: number;
}

interface IconDefinition {
  lines: [IconLine, IconLine, IconLine];
  rotation?: number;
  group?: string;
}

Collapsed Lines

When an icon needs fewer than three lines, unused lines collapse to a single point at the center with zero opacity:
const CENTER = 7;

const collapsed: IconLine = {
  x1: CENTER,
  y1: CENTER,
  x2: CENTER,
  y2: CENTER,
  opacity: 0,
};

Example Icons

menu: {
  lines: [
    { x1: 2, y1: 3.5, x2: 12, y2: 3.5 },
    { x1: 2, y1: 7, x2: 12, y2: 7 },
    { x1: 2, y1: 10.5, x2: 12, y2: 10.5 },
  ],
}

Minus Icon (1 line + 2 collapsed)

minus: {
  lines: [
    { x1: 2, y1: 7, x2: 12, y2: 7 },
    collapsed,
    collapsed
  ],
}

Check Icon (2 lines + 1 collapsed)

check: {
  lines: [
    { x1: 2, y1: 7.5, x2: 5.5, y2: 11 },
    { x1: 5.5, y1: 11, x2: 12, y2: 3 },
    collapsed,
  ],
}

Rotation Groups

Icons with shared base shapes can rotate between variants instead of morphing lines. This is controlled by the group property:
// Plus and cross share the same lines but rotate
const plusLines: [IconLine, IconLine, IconLine] = [
  { x1: 7, y1: 2, x2: 7, y2: 12 },
  { x1: 2, y1: 7, x2: 12, y2: 7 },
  collapsed,
];

plus: { lines: plusLines, rotation: 0, group: "plus-cross" },
cross: { lines: plusLines, rotation: 45, group: "plus-cross" },
When transitioning between icons in the same group, the component rotates the existing lines rather than morphing them:
const shouldRotate = useMemo(() => {
  const prev = prevDefinitionRef.current;
  return prev.group && definition.group && prev.group === definition.group;
}, [definition]);

Animation Implementation

The morphing effect uses motion’s spring physics to animate line coordinates:
function AnimatedLine({ line, transition }: AnimatedLineProps) {
  return (
    <motion.line
      animate={{
        x1: line.x1,
        y1: line.y1,
        x2: line.x2,
        y2: line.y2,
        opacity: line.opacity ?? 1,
      }}
      transition={transition}
      strokeLinecap="round"
    />
  );
}
Default transition uses a custom easing curve for smooth, natural movement:
const defaultTransition: Transition = {
  ease: [0.19, 1, 0.22, 1],
  duration: 0.4,
};

Accessibility

The component respects user motion preferences:
const reducedMotion = useReducedMotion() ?? false;
const activeTransition = reducedMotion ? { duration: 0 } : transition;

Usage Example

import { MorphingIcon } from "@/components/morphing-icon";

function IconButton() {
  const [icon, setIcon] = useState("menu");

  return (
    <button onClick={() => setIcon(icon === "menu" ? "cross" : "menu")}>
      <MorphingIcon icon={icon} size={24} strokeWidth={1.5} />
    </button>
  );
}

Available Icons

The system supports 27 icons:
  • Navigation: menu, cross, chevron-right, chevron-down, chevron-left, chevron-up, arrow-right, arrow-down, arrow-left, arrow-up
  • Actions: plus, minus, check, play, pause, download, upload, external
  • Symbols: equals, asterisk, more, grip, slash, corner

Reference

Inspired by Benji’s experiments with Claude, demonstrating how AI-assisted design can explore systematic constraints that lead to elegant solutions.

Build docs developers (and LLMs) love