morphicons

Showcase

Iconify-style pipelines render icons as CSS masks: a span whose shape is a mask-image, no path in the DOM. The mask adapter morphs that span as it is. svgToIcon reads the markup, maskTarget drives the mask, and your element keeps its classes, its paint and its place in the layout.

Why this path
You already ship icons as CSS masks: UnoCSS presetIcons, Tailwind icon plugins, plain Iconify. This animates the elements you have instead of asking you to switch to inline SVG. Any stroke-drawn set Iconify carries qualifies.
How it works
svgToIcon turns markup into morphable data, re-gridding off-grid sets via their viewBox. maskTarget keeps a hidden pair of SVG mask buffers and flips mask-image between them each frame, which is what keeps Safari repainting. Whatever paints the element shows through the moving shape.
The tradeoff
Every frame re-rasterizes the mask on the main thread, so budget it like a hover effect: toggles, nav, empty states. For icon-heavy views, inline SVG stays leaner. Client-only by nature, and fill-drawn sets (Material Symbols) are rejected up front.

One element, no path in the DOM

The shape below is a plain span: its geometry is a referenced SVG mask the driver rewrites per frame, and its color is just CSS showing through.

Shape
Element paint

The morph never touches color. A gradient, an image, plain currentColor: whatever paints the element shows through the moving mask.

The whole integration

Two adapter calls around the plain DOM driver. The element is anything with a box: a span from UnoCSS presetIcons, a Tailwind icon plugin, or your own markup.

morphicons/adapters
import { createMorph } from "morphicons/dom";
import { maskTarget, svgToIcon } from "morphicons/adapters";

// what an Iconify pipeline ships: markup, not components
const MENU = svgToIcon(
  '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M4 12h16"/><path d="M4 18h16"/><path d="M4 6h16"/></svg>',
);
const X = svgToIcon(
  '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>',
);

const target = maskTarget(el); // el: your mask-styled <span>
const morph = createMorph(target, MENU);
morph.morphTo(X, "snappy");

// on unmount: morph.destroy(); target.dispose();