Bruno DelorenceBruno
Appearance
Appearance

Choose your reading theme.

Contact

Icon morphs

New

Line icons that act out what they mean: a house builds itself, an arrow drops into a tray, a screen turns into a phone, a battery charges.

icon-morphs/icon-morphs.tsx
Live · interactive

The live component, numbered part by part. Every measurement is read from it as it is drawn. Point at a row to outline its part, or at a part to find its row.

When to use

  • A showcase of motion: a landing section, a style guide or a portfolio piece that shows how icons can act out what they mean.
  • Teaching what an action does before anyone takes it: the arrow lands in the tray, the battery fills.
  • A gallery the reader can poke at: hover, focus or press a card and its pair plays once more.
  • A still poster of the same drawings: pose holds every icon on one frame, with no loop running.

When not to use

  • Icons inside buttons, menus or toolbars. Use a static icon set; a 2 s animation there only slows the reader down.
  • Progress or status that must be true: the battery here charges on a loop, not from a real level. Use a progress bar or a meter.
  • Any icon outside these four pairs: the drawings are keyed by hand in icons.ts. Draw a Lottie or an animated SVG for new ones.
  • Pages that already move a lot: four looping cards compete with any other animation for the eye.

You can add this component with the shadcn CLI or by hand:

Into components/icon-morphs of any React project, with no setup:

Terminal
npx shadcn@latest add https://brunodelorence.com/r/icon-morphs.json

Either way it needs React 19 and a bundler that supports CSS Modules, such as Next.js or Vite. It has a dark colour scheme: it follows your page’s color-scheme, light unless the page sets one, or its colorScheme prop, with nothing to configure. Browsers without light-dark() show it light.

Usage

showcase.tsx
import { IconMorphs } from './icon-morphs';export function Showcase() {  return <IconMorphs autoplay={false} />;}

Each state below is a live instance, locked so you can study it.

The pieces it is built from, each with its measurements read live.

Four grey cards, each with a pair of line icons that play a short, springy animation: one house goes up bar by bar while the other unfolds like a jointed ruler, a download arrow sinks into a tray that sags and springs back as its upload twin lifts off an arch, a screen tips over into a phone or narrows into one, and a battery fills through a cable or from a plug that knocks it sideways. They loop in the demo; hover, focus or press a card to play its pair again.

  • Each icon is a pure function of time: given a frame, it returns the SVG paths to draw. One requestAnimationFrame loop asks every moving icon for its frame and writes the paths straight into a small pool of <path> elements, so React renders the cards once and the loop sleeps whenever nothing moves.
  • The motion lives in keyed curves: values at given frames (sixtieths of a second), joined by a monotone cubic so a curve never overshoots its own keys. Wind-ups, squashes, overshoots and settles are all written into the keys.
  • Everything is drawn on a 290-unit tile with lines 21 units thick, 7 px on the 97 px tile. Lines have round caps and joins; the trays and bars that change shape are filled outlines, their corners drawn as cubic quarter circles so they stay true under a rotation.
  • Home (2.2 s): the left house grows from a dot into a bar, the bar rises into walls, the ridge lifts into a roof and the door bounces in, tall and narrow then short and wide. The right house is one jointed line: each wall, rafter, post and lintel swings on its own spring from the part it hangs on.
  • Download · Upload (1.43 s): the arrow drops 77 units; the tray under it deepens and rounds off, then flings it 43 units back up while turning shallow and narrow, and both settle. Upload mirrors it: the base curls into an arch under the rising arrow, then drops flat and wide.
  • PC · Phone (2.4 s): the left screen winds back 5°, turns a quarter, overshoots by 4° and settles as a phone, its stand climbing in to become the speaker; the right one narrows and grows tall instead. Halfway through, both play the same curves mirrored back to the screen.
  • Battery (2.2 s): on the left a cable fades in from above and the charge rises with a sloshing surface, darkening as it fills, before the cable sinks away. On the right the charge slides in from the edge as a plug and knocks the battery 11 units sideways and 2° over, its cap swinging after it. Both drain from the top.
  • Each pair keeps its own clock and rests on its finished pose: the house built, the arrows still, the screen, the battery charged. While on screen and untouched every pair loops; on any real input a moving pair finishes its loop and rests, and hovering, focusing or pressing a card plays it once more.
  • Every colour is a light-dark() token on the root, so the cards follow the page’s color-scheme, or the colorScheme prop.
PropTypeDefaultDescription
autoplaybooleantrueLoop every pair while the cards are on screen and untouched. Off, each pair rests and plays once when hovered, focused or pressed.
colorScheme'light' | 'dark' | 'system'—Its colours: light, dark, or the reader’s system setting. Without it the component follows the page’s color-scheme, which is light unless the page sets one.
posenumber—Holds every icon still at this point of its loop, from 0 (the finished, resting pose) to 1, for a poster or a screenshot. Turns the demo and the replays off.
classNamestring—Extra classes for the root.

The complete source behind this demo. Open source under the MIT license, © 2026 Bruno Delorence: copy it, change it, ship it.

icon-morphs.tsx
// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import { useEffect, useId, useRef, useState } from 'react';import { LINE, PAIRS, type Icon, type Pair, type Shape } from './icons';import { useAutoplay } from './use-autoplay';import styles from './icon-morphs.module.css';/* * Icon morphs: four pairs of line icons on grey cards, each icon a small, playful animation — * a house that builds itself and one that unfolds, an arrow that drops into a tray and one that * springs off an arch, a screen that turns into a phone, a battery that charges and drains. * * - Every icon is a pure function of time (icons.ts): given a frame, it returns the SVG paths to *   draw. One requestAnimationFrame loop asks each moving icon for its frame and writes the *   paths straight into its <path> elements, so React renders the cards once and never again. * - Each pair keeps its own clock. While the demo is on screen and untouched, every pair loops. *   Any real input hands the cards to the reader: a pair that is moving finishes its loop and *   rests, and hovering, focusing or pressing a card plays its pair once more. * - At rest an icon shows its finished pose (the house built, the battery charged), which is also *   what the server renders and what reduced motion keeps. * - Light or dark: every colour is a light-dark() token in the CSS, so the scheme is the page's *   color-scheme, or the one the colorScheme prop sets.
icons.ts
// Copyright (c) 2026 Bruno Delorence. MIT License./* * The eight drawings of the icon morphs, and how each one moves. * * Every icon is a pure function of time: `draw(t)` returns the shapes to paint at frame `t` (the * clock counts in sixtieths of a second, so a 2.2 s loop is 132 frames). The shapes are plain * SVG path strings, stroked with the line weight or filled, so one generic painter can draw any * frame without React. * * Everything is drawn on a 290-unit tile with lines 21 units thick (7 px on the 97 px tile). * Motion comes from keyed curves: values at given frames, joined by a monotone cubic so a curve * never overshoots its own keys; anticipation, squash, overshoot and settle are all in the keys. *//** The line weight, in tile units. */export const LINE = 21;const HALF = LINE / 2;export type Shape = {  /** 's' strokes the path with the line weight, 'f' fills it. */  kind: 's' | 'f';  d: string;  opacity?: number;
use-autoplay.ts
// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import { useCallback, useEffect, useRef, useState } from 'react';/* * When a demo may play on its own: while part of it is on screen and the tab is visible, never * under reduced motion, and not while the reader is using it. `take()` hands it to the reader; it * takes back over `idle` ms after their last input. */export function useAutoplay<T extends HTMLElement>(idle = 6000) {  const ref = useRef<T>(null);  const [visible, setVisible] = useState(false);  const [reduced, setReduced] = useState(true);  const [taken, setTaken] = useState(false);  const timer = useRef(0);  useEffect(() => {    const element = ref.current;    if (!element) return;    let onScreen = false;    const update = () => setVisible(onScreen && !document.hidden);    const observer = new IntersectionObserver(
icon-morphs.module.css
/* Icon morphs (icon-morphs.tsx): four grey cards on a white stage, two white tiles on each with   an animated line icon, and the pair's name under them. Two cards a row, or one on a narrow   stage (a container query).   Colours are the tokens below, light-dark(light, dark) where the two differ: the component takes   the color-scheme it inherits from the page (light unless the page says otherwise), or the one   its colorScheme prop sets (data-color-scheme).   Memory. The drawings are SVG paths repainted in place by one animation loop, and only while a   pair moves; each tile clips its own drawing, so a repaint stays inside its 97 px square. */.root {  /* The stage, the cards, and the tiles on them. */  --surface: #fdfdfd;  --card: #f2f2f2;  --tile: #fefefe;  /* The icons and the names. */  --ink: #2b2b2b;  /* The stage's edge. */  --line: rgb(0 0 0 / 0.06);  /* The same on both schemes: the focus ring. */  --focus: #387bff;  container-type: inline-size;  box-sizing: border-box;
  • The cards are a list of buttons named “Replay Home”, “Replay Download · Upload” and so on, each containing its visible name; Tab reaches them, and focusing or pressing one plays its pair.
  • The drawings are decorative and hidden from assistive technology; the names say what each pair shows.
  • Reduced motion shows every icon in its finished pose and never animates it. The loop pauses offscreen and in a hidden tab, and stops when the reader takes over.