Morph: Transform Without Tearing Down and Rebuilding
Fading out then fading in is the safest way to handle a state change — and also the least informative. The user has no idea whether the old object and the new object are related. Morphing solves this: the same “entity,” but every property about it is changing in real time.
Click Next shape — 24 dots rearrange into Circle, Triangle, Grid
Why morphing works with the human brain
When one object disappears and another appears, the human brain treats them as two separate objects. When the same object moves and changes, the brain tracks it as one continuous entity — object permanence.
Morphing exploits object permanence to convey information: “this is still that thing, just in a different state.” This is especially valuable when the new state has a semantic relationship to the old one — like play/pause, expanded/collapsed, or this article itself: 12 constellations from the same set of stars.
The core mechanism: interpolation
At its simplest, morphing is a lerp (linear interpolation) between two sets of coordinates:
// Each frame in the animation loop
const x = source.x + (target.x - source.x) * progress;
const y = source.y + (target.y - source.y) * progress;
Here progress is a value from 0 to 1 that advances over time. Use easing to make progress non-linear:
function easeInOut(t: number) {
return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
}
const progress = easeInOut(Math.min(1, elapsed / duration));
easeInOut makes the object start slowly, speed up in the middle, then slow down as it reaches the target — much closer to real-world physics than linear.
The mismatched-count problem
Morphing is easy when the number of elements stays the same. It’s harder when state A has 4 dots and state B has 9 dots. There are three ways to handle it:
Spawn from the nearest point: a new dot appears at the position of an existing dot, then moves to its target. It looks natural because the dot “splits off” from the cluster.
// Extra dots: spawn at the nearest existing dot
for (let i = nCurr; i < nNext; i++) {
const src = dots[i % nCurr]; // use modulo to pick a source
dots.push({ sx: src.x, sy: src.y, tx: targets[i].x, ty: targets[i].y });
}
Collapse to the center: surplus dots from state A move into the center and fade out. The opposite of spawning — dots “dissolve” into each other instead of splitting apart.
Partial cross-fade: for a large difference in count, fade out the entire old group and fade in the new group — a hybrid between morph and replace.
A state machine for complex morphs
A simple animation only needs an isAnimating flag. A multi-state morph needs an explicit state machine to avoid race conditions:
type Phase = 'idle' | 'fade_out' | 'morph' | 'fade_in';
let phase: Phase = 'idle';
let phaseStart = 0;
function frame(now: number) {
const t = now - phaseStart;
if (phase === 'fade_out' && t > FADE_DUR) {
prepareTargets(); // set up targets for the new state
phase = 'morph';
phaseStart = now;
return; // return so the next frame starts with t = 0
}
if (phase === 'morph' && t > MORPH_DUR) {
snapToTargets(); // lock to the final positions
phase = 'idle';
phaseStart = now;
return;
}
// draw based on current phase...
}
Important note: return immediately after changing phase. Otherwise, the drawing code right below will run with the old t (before phaseStart is reset) — causing the animation to jump abruptly for one frame.
Alpha morph: more than just position
Morphing isn’t only about position. Opacity, scale, and color can all interpolate at the same time. A new dot appears by fading in while it moves:
// New dot (born): alpha from 0 → 1 during the morph
if (dot.born) {
alpha = easeInOut(morphProgress);
}
// Extra dot (dying): alpha from 1 → 0
if (dot.dying) {
alpha = 1 - easeInOut(morphProgress);
}
Use the same easeInOut for both alpha and position so they stay in sync — a dot fades out exactly as it arrives, and a new dot brightens up from its starting point.
When not to use morphing
Morphing fits when there’s semantic continuity: the same thing in a different state.
It doesn’t fit when:
- The content is entirely unrelated (tab A → tab B on a different topic)
- The element counts differ too much (3 dots → 50 dots)
- The user needs to clearly understand that “the old thing is gone, the new thing is completely different”
In those cases, a fade or a slide with spatial direction conveys more information.
Real-world examples
Play → Pause icon: morph the lines of the play button (a triangle) into the two vertical bars of pause. SVG path morphing uses the same number of anchor points.
Search → Close: the magnifying-glass icon morphs into an X — the circle shrinks and the handle rotates into the second diagonal stroke.
Zodiac constellations: 12 different constellations use the same set of “stars” — when you switch signs, the stars move to new positions instead of disappearing. The user perceives this as one continuous system, not 12 separate images.