Getting started
Install Morph, make a morph, and draw any moment of it: in React, in the page, or on a server.
npm install @cesarvenzac/morphPlain TypeScript, published as ES modules with their types, and no dependencies. React is an optional peer, for the React entry alone, and only the DOM entry needs a browser.
A first morph
A morph is made from two sources and drawn at any moment between them. Making it is the costly part, where the pieces are matched, so make it once and keep it:
import { morph } from "@cesarvenzac/morph";
import { Plus, X } from "lucide";
const m = morph(Plus, X);
const frame = m.at(0.5); // halfwayat(t) takes any t from 0, the first side exactly, to 1, the second exactly, and returns the frame as parts: a <path> each, its path data and how it's painted. Between, t can come from anywhere: a spring, a tween, a scroll position, a slider.
import { morph } from "@cesarvenzac/morph";
import { Plus, X } from "lucide";
const m = morph(Plus, X); // the costly part, done once
// Any moment from 0 to 1, as SVG path data a stroke, each with
// its share of the icon's stroke width.
m.at(t); // [{ d: "M4.88 14.95C…", width: 1 }, …]
Draw it
In React, MorphFrame draws a morph at the t you give it, and useMorph keeps the morph across renders, made again only when a side or the options change:
import { MorphFrame, useMorph } from "@cesarvenzac/morph/react";
import { Sun, Moon } from "lucide";
export function Dial({ t }: { t: number }) {
const m = useMorph(Sun, Moon);
return <MorphFrame morph={m} t={t} size={48} />;
}Without React, draw puts a frame into an <svg> on the page, a <path> a part, and svgOf writes one as an SVG document, for a server, an <img> or a file:
import { svgOf } from "@cesarvenzac/morph";
import { draw } from "@cesarvenzac/morph/dom";
draw(document.querySelector("svg")!, m.at(t), m.viewBox);
svgOf(m, 0.5); // '<svg xmlns="http://www.w3.org/2000/svg" …'Play it
For an icon that changes with a state, as a button's does, MorphIcon springs to each new icon, and turns back from where it is when it's sent back mid-way:
import { MorphIcon } from "@cesarvenzac/morph/react";
import { Menu, X } from "lucide";
<button
onClick={() => setOpen(!open)}
aria-label={open ? "Close menu" : "Open menu"}
>
<MorphIcon icon={open ? X : Menu} />
</button>;Name the button, not the icon: MorphIcon is hidden from assistive tech unless it's given a name of its own. Under prefers-reduced-motion it changes at once. Playing it has the rest: the spring, mount for the DOM, and motion for any other clock.
Beyond icons
Either side can be any SVG, read from its markup, or a word set in a font: What it morphs has every kind.
import { morph, svg, text } from "@cesarvenzac/morph";
morph(svg(heartMarkup), svg(starMarkup));
morph(text(font, "Hello"), text(font, "World"));