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, andviewBox. 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 from0to1. The default controlled position is0.spring("smooth" | "snappy" | "bouncy" | object): Selects a preset or custom spring. The default behavior usessnappy.size(number | string): Sets SVG width and height. The default is24.color(string): Sets the SVG stroke color. The default iscurrentColor.strokeWidth(number | string): Sets the stroke width. The default is2.absoluteStrokeWidth(boolean): Keeps the visual stroke width consistent when the icon size changes. The default isfalse.label(string): Addsrole="img"and a<title>element. An omitted label marks the SVG as decorative througharia-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:
- SVG Path Morph Animation Library – svg-path-morph
- Material Design Inspired SVG Icons Morphing Effects – SVG Morpheus
- Animated Icon Component Library for React/Vue/Svelte/Solid/Web Component
- Smooth Animated UI Icons For React & TypeScript – pqoqubbw/icons
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.