morphicons: SVG Icon Morphing for JavaScript, React, and Vue

Category: Animation , Javascript , Recommended | August 13, 2026
Authorguillermolg00
Last UpdateAugust 13, 2026
LicenseMIT
Tags
Views32 views
morphicons: SVG Icon Morphing for JavaScript, React, and Vue

morphicons is an ESM SVG icon morphing library that animates one stroke-based icon into another using spring physics.

It accepts Lucide-style icon data or raw SVG path data and provides dedicated entries for React, React Native, Vue, plain JavaScript, and DOM-free geometry work.

The library calculates path correspondence, rotation, scale, and translation from the icon geometry.

For example, menu controls fold into close icons, arrows rotate toward new directions, and rapid state changes retarget active animations from their current shapes.

See it in Action:

Features:

  • Universal morphing between compatible stroke icons.
  • Automatic rotation and scale detection.
  • Smooth spring motion with three built-in presets.
  • Interruptible animation during rapid state changes.
  • Controlled progress for drag, scroll, and scrub interfaces.
  • React and component bindings.
  • Direct DOM and pure geometry modules.
  • Static SVG output for server rendering.
  • Reduced-motion handling through instant icon swaps.
  • Shared animation scheduling across active instances.
  • Raw path and Lucide-style icon data support.

How To Use It:

Installation

Install the package with package managers. React 18 or newer is required for morphicons/react. Vue 3.3 or newer is required for morphicons/vue. Both frameworks are optional peer dependencies, and the core package has no runtime dependencies.

Icon data must come from a raw SVG d string or a Lucide-style node array. Import Lucide icon data from lucide. Imports from lucide-react and lucide-vue-next return rendered components and do not match the input format.

npm install morphicons
pnpm add morphicons
bun add morphicons

Basic Usage With React

A sound toggle only needs component state and one icon prop. Changing that prop starts the morph automatically.

import { useState } from "react";
import { MorphIcon } from "morphicons/react";
import { Volume2, VolumeX } from "lucide";
export default function SoundToggle() {
  const [muted, setMuted] = useState(false);
  return (
    <button
      type="button"
      aria-pressed={muted}
      aria-label={muted ? "Turn sound on" : "Mute sound"}
      onClick={() => setMuted((value) => !value)}
    >
      <MorphIcon
        icon={muted ? VolumeX : Volume2}
        spring="snappy"
      />
    </button>
  );
}

Controlled Progress In React

Drag handles and scroll-linked controls need direct progress instead of spring animation. Controlled mode accepts a source icon, a target icon, and a value from 0 to 1.

import { MorphIcon } from "morphicons/react";
import { ArrowLeft, ArrowRight } from "lucide";
<MorphIcon
  from={ArrowLeft}
  to={ArrowRight}
  progress={dragProgress}
  size={32}
  strokeWidth={1.75}
/>

Vue Usage

The Vue binding supports uncontrolled, controlled, and imperative modes. Standard SVG attributes, classes, and styles pass to the root SVG element.

<script setup lang="ts">
import { ref } from "vue";
import { MorphIcon } from "morphicons/vue";
import { Bell, BellOff } from "lucide";
const notificationsPaused = ref(false);
</script>
<template>
  <button
    type="button"
-aria-pressed="notificationsPaused"
-aria-label="
      notificationsPaused
        ? 'Resume notifications'
- 'Pause notifications'
    "
    @click="notificationsPaused = !notificationsPaused"
  >
    <MorphIcon
-icon="notificationsPaused ? BellOff : Bell"
      spring="smooth"
    />
  </button>
</template>

Plain JavaScript Usage

The DOM entry attaches a morph controller to an existing SVG <path>. Your SVG element remains responsible for sizing, color, view box, line caps, and line joins.

<button id="downloadButton" type="button">
  <svg
    width="28"
    height="28"
    viewBox="0 0 24 24"
    fill="none"
    stroke="currentColor"
    stroke-width="2"
    stroke-linecap="round"
    stroke-linejoin="round"
    aria-hidden="true"
  >
    <path id="downloadIcon"></path>
  </svg>
  <span>Save file</span>
</button>
<script type="module">
  import { createMorph } from "morphicons/dom";
  import { Check, Download } from "lucide";
  const path = document.querySelector("#downloadIcon");
  const button = document.querySelector("#downloadButton");
  const iconMorph = createMorph(path, Download);
  let saved = false;
  button.addEventListener("click", () => {
    saved = !saved;
    iconMorph.morphTo(
      saved ? Check : Download,
      "snappy"
    );
  });
</script>

Fit Icons From Another View Box

Compatible icon pairs should share the same coordinate space. The React and Vue components use a 0 0 24 24 view box by default.

Run fitIcon() once at module scope when an icon uses another grid. The returned value is a raw d string that works anywhere an icon input is accepted.

import { fitIcon } from "morphicons";
// A simple cross drawn on a 32 by 32 grid.
const cross32 = "M4 16H28 M16 4V28";
// Convert the geometry to the default 24 by 24 grid.
const cross24 = fitIcon(cross32, 32);

The second argument accepts a grid size, a view box string such as "0 0 32 32", or a four-number view box array. Do not run the conversion during every component render.

Pure Core Usage

The core entry computes path data and does not access the DOM. This suits custom renderers, canvas bridges, native adapters, and exported animation frames.

import {
  allocOutputs,
  buildPlan,
  interpPolar,
  resampleIcon,
  serialize,
} from "morphicons";
import { Menu, X } from "lucide";
const plan = buildPlan(
  resampleIcon(Menu),
  resampleIcon(X)
);
const output = allocOutputs(plan);
interpPolar(plan, 0.5, output);
const pathData = serialize(
  output,
  plan.items.map((item) => item.closed)
);

pathData contains an SVG path string for the selected progress value.

Component Configuration Options

React also accepts standard SVG props such as className, style, and viewBox. Vue accepts standard SVG attributes through attribute fallthrough.

  • icon (IconInput): Sets the current icon in uncontrolled mode. A changed value starts a spring morph.
  • from (IconInput): Sets the source endpoint in controlled mode.
  • to (IconInput): Sets the target endpoint in controlled mode.
  • progress (number): Sets controlled morph progress from 0 to 1. The default controlled position is 0.
  • spring ("smooth" | "snappy" | "bouncy" | object): Selects a preset or custom spring. The default behavior uses snappy.
  • size (number | string): Sets SVG width and height. The default is 24.
  • color (string): Sets the SVG stroke color. The default is currentColor.
  • strokeWidth (number | string): Sets the stroke width. The default is 2.
  • absoluteStrokeWidth (boolean): Keeps the visual stroke width consistent when the icon size changes. The default is false.
  • label (string): Adds role="img" and a <title> element. An omitted label marks the SVG as decorative through aria-hidden.

Spring Presets And Custom Physics

  • smooth: Critically damped motion with no overshoot.
  • snappy: Fast motion with subtle overshoot.
  • bouncy: Softer damping with visible rebound.

Custom physics use stiffness and damping. A larger stiffness value increases the restoring force. A larger damping value reduces rebound.

<MorphIcon
  icon={activeIcon}
  spring={{
    stiffness: 360,
    damping: 26,
  }}
/>

API Methods

React refs and Vue template refs expose two imperative methods.

// Animate to another icon.
// The second argument accepts a preset or custom spring.
iconRef.current?.morphTo(Check, "bouncy");
// Replace the current icon immediately.
iconRef.current?.set(Download);

The plain JavaScript controller exposes the full DOM method surface.

// Animate toward an icon with a preset.
iconMorph.morphTo(Check, "snappy");
// Animate with custom spring physics.
iconMorph.morphTo(Check, {
  stiffness: 360,
  damping: 26,
});
// Replace the current icon immediately.
iconMorph.set(Download);
// Render a frozen intermediate shape.
iconMorph.seek(Check, 0.45);
// Read the last rendered progress value.
console.log(iconMorph.progress);
// Seek toward the active target.
iconMorph.progress = 0.75;
// 2D canvas out
iconMorph.canvasTarget(canvas | ctx, opts?)
// Stop future work and unregister the instance.
iconMorph.destroy();

Alternatives:

FAQs:

Q: Why does a custom icon render outside the SVG view box?
A: The two icons probably use different coordinate grids. Convert the off-grid icon with fitIcon() and keep the component view box aligned with the converted geometry.

Q: Can morphicons animate filled icon packs?
A: The parser accepts many filled paths. The interpolation system targets stroke centerlines, and filled icon packs usually lose visual clarity during the transition.

Q: Why does an imported Lucide component fail as an icon input?
A: The component bindings expect icon geometry data. Import icons from lucide. Imports from lucide-react and lucide-vue-next return rendered components.

Q: Does morphicons support accessible icons?
A: Pass label when the icon conveys meaning. The component adds an image role and SVG title. An omitted label produces a decorative SVG with aria-hidden.

Q: Why does the animation change instantly?
A: Your user may have enabled reduced motion, or the next icon already matches the active target. Reduced-motion mode replaces the icon immediately.

Q: Does the animation work with server-side rendering?
A: Yes. The server renders the exact static SVG for the current icon, and the animated runtime attaches on hydration with no layout shift.

Changelog:

v1.7.0 (08/13/2026)

  • Added morphicons/element — the <morph-icon> custom element.
  • Added morphicons/astro — Astro binding as an SSR shell over the element.

v1.6.0 (08/08/2026)

  • Added canvasTarget(canvas | ctx, opts?).

v1.5.0 (08/07/2026)

  • Added morphicons/adapters — format adapters, one opt-in entry.

v1.4.0 (08/04/2026)

  • Added morphicons/react-native — React Native binding.

You Might Be Interested In:


Leave a Reply