Skip to content

IconSwap

Transition between icons to show a change of state, including morphing one shape into another.

Use it when a control's two states are two different icons: notify and muted, invite and invited, a file and the link that opens it. It takes Youj icons and lucide-react icons alike. When a state icon already covers the pair, use that instead: its morph is choreographed for those two states.

Installation

npm i @youj/icons

import { IconSwap } from '@youj/icons'

For an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.

Usage

Give it the two icons and a state. active={false} shows from, true shows to.

import { BellIcon, BellOffIcon, IconSwap } from '@youj/icons'

<IconSwap from={BellIcon} to={BellOffIcon} active={muted} transition="morph" />

Pass the components, not elements. Youj icons are held still inside the swap, so only the transition moves.

Examples

Transitions

Every transition is a pure function of one progress value, from 0 (showing from) to 1 (showing to). Toggling mid-swap reverses from exactly where it is, and rapid toggles never jump. The default is scale-blur.

TransitionWhat it does
scale-blurThe old icon shrinks out of focus as the new one sharpens in.
rotateOne continuous quarter turn: out turns away as in turns in from the other side.
morphA true path morph from one icon's strokes to the other's.
drawThe old strokes run out through their ends, then the new strokes draw on.
flipA card turned on its vertical axis.
foldHinged flaps: out folds back on its bottom edge, in swings down from the top.
slideOne strip: the new icon pushes the old one out through the edge of the box.
wipeA hard edge sweeps across, with the new icon behind it.
zoomOut flies past the viewer, in arrives from far away.
popOut shrinks away, in overshoots once and settles. Keep it for success.

direction steers slide (default up) and wipe (default right). Left and right follow the reading direction, so they swap under dir="rtl".

Chain up to ten icons in any transition, and copy the code, in Chain.

Reference

Propsfrom, to, active… (12)

Props

IconSwap props
PropType
fromRequiredSwapIcon

Shown while inactive.

–
toRequiredSwapIcon

Shown while active.

–
activeboolean

Controlled state: false shows from, true shows to. Overrides the trigger.

–
triggerIconTrigger
'hover''mount''loop''none'

What swaps an uncontrolled IconSwap.

default 'hover'

'hover'
progressnumber | MotionValue<number>

Drives the swap directly instead of a state: 0 shows from, 1 shows to, and a value between holds that moment of the transition (a scrubber, scroll progress). A MotionValue repaints on every change without re-rendering. When set, active and trigger are ignored, and the state (labels, data-state) is whichever icon it's nearer. It's painted as given under reduced motion too, as ScrollProgress tracks scroll: the value is the caller's, and the swap adds no motion of its own.

–
transitionIconSwapTransition
'rotate''scale-blur''draw''flip''slide''fold''wipe''zoom''pop''morph'

default 'scale-blur'

'scale-blur'
directionIconSwapDirection
'up''down''left''right'

Travel direction for slide (default up) and wipe (default right). Left and right follow the reading direction: under dir="rtl" they swap.

–
labelsreadonly [string, string]

Accessible names for [from, to]. Without them the swap is decorative (aria-hidden), e.g. inside a labeled button.

–
sizenumber | string

default 24

24
strokeWidthnumber

default 1.5

1.5
classNamestring–
styleCSSProperties–
MotionScale-blur, rotate, zoom, slide, wipe, flip, fold… · reduced motion

Motion

Motion tokens
MomentTokens
scale-blur, rotate, zoomout on exit, in on enter
ease.enter

cubic-bezier(0.16, 1, 0.3, 1) · slow + fast: 480ms

cubic-bezier(0.16, 1, 0.3, 1)slow + fast: 480ms
slide, wipeone push across the box
ease.emphasized

cubic-bezier(0.6, 0, 0.1, 1) · slow + fast: 480ms

cubic-bezier(0.6, 0, 0.1, 1)slow + fast: 480ms
flip, foldout to edge-on, then in
duration.deliberateease.enter

560ms · cubic-bezier(0.16, 1, 0.3, 1)

560mscubic-bezier(0.16, 1, 0.3, 1)
popin overshoots to 114%, then settles
duration.deliberateease.enter

560ms · cubic-bezier(0.16, 1, 0.3, 1)

560mscubic-bezier(0.16, 1, 0.3, 1)
drawstrokes retract, then new ones draw on
duration.deliberateease.enter

560ms · cubic-bezier(0.16, 1, 0.3, 1)

560mscubic-bezier(0.16, 1, 0.3, 1)
morphstrokes bend into place
ease.emphasized

cubic-bezier(0.6, 0, 0.1, 1) · deliberate + fast: 700ms

cubic-bezier(0.6, 0, 0.1, 1)deliberate + fast: 700ms
Reversaltakes the remaining share of the length
no token

length × distance left

length × distance left

Reduced motion

The swap is instant: the new icon replaces the old one with no transition.

Reduced motion

The swap switches instantly to the icon for the new state. Nothing moves, and the state reads the same.

AccessibilityKeyboard, ARIA, focus

Accessibility

  • Without labels, the swap is decorative (aria-hidden). Inside a button, put the name and the state on the button, with aria-pressed or a name that changes, as useIconState does.
  • With labels, it's role="img" named for its current state. A new name isn't announced on its own, so pair a change that matters with a status message.
  • The swap never takes focus, and its hover trigger also answers keyboard focus on the parent.