Icon Morph
loomui/icon-morphFreeComponent
One icon that turns into another by moving its own pieces, so there is never a frame with both glyphs on screen.
Live preview
live · rendered by the registry
Rendered by loomui on their own page — this is the component running, not a screenshot of it.Install it
npx shadcn@latest add https://loomui.design/r/icon-morph.jsonSource
1"use client"23import * as React from "react"45import { cn } from "@/lib/utils"67/** Which pair of shapes the icon travels between. */8export type IconMorphSet = "menu" | "plus" | "play" | "chevron"910export interface IconMorphProps extends Omit<11 React.ComponentProps<"svg">,12 "children"13> {14 /** Which pair of shapes to morph between. */15 set?: IconMorphSet16 /** `false` shows the first shape, `true` the second. */17 active?: boolean18 /** Milliseconds for the morph. Set to `0` to turn it off. */19 duration?: number20 /** Stroke weight, in viewBox units. */21 strokeWidth?: number22}2324/**25 * What each `set` travels between, and what `active` means.26 *27 * | set | active false | active true |28 * | --------- | ------------ | ----------- |29 * | `menu` | hamburger | close |30 * | `plus` | plus | close |31 * | `play` | play | pause |32 * | `chevron` | chevron | tick |33 */3435/**36 * Shared by every piece that moves rather than reshapes. The duration is a37 * custom property set on the root, so one prop reaches every part without38 * being threaded through them.39 *40 * `motion-reduce:transition-none` is a class rather than an inline style on41 * purpose: an inline `transition` would outrank it and the escape would not42 * work.43 */44const SHIFT =45 "transition-[transform,opacity] ease-out-quart [transition-duration:var(--icon-morph-duration)] motion-reduce:transition-none"4647/**48 * SVG transforms resolve against the viewBox once `transform-box` says so.49 * Firefox and Safari have both shipped a different initial value at some50 * point, so it is stated rather than assumed.51 */52function pose(53 origin: string,54 transform?: string,55 opacity?: number56): React.CSSProperties {57 return {58 transformBox: "view-box",59 transformOrigin: origin,60 transform: transform ?? "none",61 opacity,62 }63}6465/**66 * Quartic ease out. `--ease-out-quart` is the cubic-bezier approximation of67 * this curve, so driving the tween with the curve itself keeps the reshaping68 * sets in step with the transforming ones.69 */70function easeOutQuart(t: number) {71 return 1 - (1 - t) ** 472}7374function usePrefersReducedMotion() {75 const [reduced, setReduced] = React.useState(false)7677 React.useEffect(() => {78 const query = window.matchMedia("(prefers-reduced-motion: reduce)")79 const read = () => setReduced(query.matches)80 read()81 query.addEventListener("change", read)82 return () => query.removeEventListener("change", read)83// … truncated
What it pulls in
Other registry items
Files it writes
More from loomui
All 91 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Aurora Backdropaurora-backdrop | loomui | Components | Free | no deps | |
| Bento Gridbento-grid | loomui | Components | Free | no deps | |
| Card Stackcard-stack | loomui | Components | Free | no deps | |
| Compare Slidercompare-slider | loomui | Components | Free | no deps | |
| Confetti Buttonconfetti-button | loomui | Components | Free | no deps | |
| Count Upcount-up | loomui | Components | Free | no deps | |
| Credit Cardcredit-card | loomui | Components | Free | no deps | |
| Drawerdrawer | loomui | Components | Free | 1 dep |
