Getting started
Foundations
Integrations
Form19
Display5
Layout4
Navigation4
Overlay8
Feedback4
Motion19
Animated Icon
Two icons in one place, trading with a transition instead of a swap.
#Installation
npx @dinachi/cli@latest add animated-icon#Usage
import { AnimatedIcon } from "@/components/ui/animated-icon"
import { Play, Pause } from "lucide-react"<button type="button" aria-label={playing ? "Pause" : "Play"} onClick={toggle}>
<AnimatedIcon active={playing} from={<Play />} to={<Pause />} />
</button>#Examples
#What the motion is for
The two states read as the same object changing, so the control is one toggle rather than two buttons taking turns. Swap the element outright and the eye registers a replacement; move it and the eye registers a state.
#It ships no icons
Give it any two icons and it handles the crossfade, the stacking, the sizing and the reduced-motion path. A lucide pair, your own SVG, an emoji, two spans.
<AnimatedIcon active={muted} from={<Volume2 />} to={<VolumeX />} />
<AnimatedIcon active={copied} from={<Copy />} to={<Check />} />
<AnimatedIcon active={open} from={<Menu />} to={<X />} mode="rotate" />It does not morph one path into another. That needs two paths written with the same commands in the same order, and no two icons out of a library ever are. A morph between mismatched paths does not degrade, it fails. The transform pair works for every combination instead of for the handful that happen to be compatible.
#Modes
| Mode | What happens |
|---|---|
scale | The outgoing icon drops to 0.7 and fades; the incoming one rises from it. The default. |
rotate | Adds a quarter turn each way, so the icon reads as turning over. |
flip | Rotates about the horizontal axis, with a vanishing point. |
fade | Opacity only. For pairs where any movement would be noise. |
exit mirrors initial rather than repeating it, so the icon that leaves goes the way the
next one is coming from and the pair reads as one movement rather than as two.
#Timing
150ms, no bounce, no delay. These sit on controls someone presses dozens of times a day, and the motion must never come between the press and the answer. Anything springier here would be felt as lag rather than as character.
#Sizing
The box owns the size and both icons fill it. Default size-5.
<AnimatedIcon active={dark} from={<Sun />} to={<Moon />} className="size-8" />Do not size the icons individually. An icon library ships an intrinsic 24×24, which a smaller box squashes on one axis and a larger one leaves floating in the middle. Two icons sized apart are two icons that jump as they trade.
#When to use it
| Use it for | Not for |
|---|---|
| A control with two states the reader toggles: a play button, a mute button, a disclosure. | Icons that never change. There is no second state to travel to. |
| Confirmation, once, at the end of an action: copy becoming a check. | Decoration. An icon that animates for its own sake on a page someone visits daily is noise. |
#Implementation notes
- Nothing appears from nothing.
scalestarts at 0.7, not 0. initial={false}on theAnimatePresence, so the icon is simply present on first paint rather than animating in on every mount during hydration.perspectiveonly underflip. A rotation about an axis in the plane of the screen is invisible without a vanishing point; the other three do not need one and should not pay for one.- One grid cell, not absolute positioning. Both icons occupy the same cell, so nothing reflows mid-transition and neither one has to be measured.
#Accessibility
- It carries
aria-hidden. It is decoration inside a control, and the control is what gets named. - Name the button, not the icon. If the button's label does not change with its state,
put the state in
aria-expandedoraria-pressed. - Reduced motion cuts to the end state. Which icon is showing is the information; watching it arrive is not.
#API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
| active | boolean | — | Which of the two is showing. false shows from, true shows to |
| from | ReactNode | — | The resting icon. Any element: a lucide icon, an inline svg, an emoji |
| to | ReactNode | — | The icon for the active state |
| mode | "scale" | "rotate" | "flip" | "fade" | "scale" | How the two trade places |
| className | string | "size-5" | Sizes and colours both icons. The shell owns the size; do not size the icons individually |