Morphs
Show a control’s state with icons that morph between meanings, such as play and pause.
Use a state icon when the icon itself should show which state a control is in: a menu that opens, a track that plays, a microphone that's muted, a page that's saved. When the two states are two different icons with no state icon between them, use IconSwap. For a one-off confirmation, like a check after a save, a plain icon with trigger="mount" says it with less.
Installation
State icons ship with the rest of the set.
npm i @youj/iconsOr copy one in with the Youj CLI. Each is its own item and brings the icon engine with it.
npx @youj/cli add icon-menuUsage
Pass your state to active. false is the first state, true the second.
import { useState } from 'react'
import { MenuIcon } from '@youj/icons'
export function NavToggle() {
const [open, setOpen] = useState(false)
return (
<button type="button" aria-expanded={open} aria-controls="site-nav" aria-label="Menu" onClick={() => setOpen(!open)}>
<MenuIcon active={open} />
</button>
)
}Each icon names its two states in meta.states, for example MenuIcon.meta.states is ['menu', 'close']. The morph shows the state, and the button carries it for assistive technology, here with aria-expanded.
Examples
Three kinds of morph
Every pair is choreographed on its own, and the choreographies fall into three kinds.
- Shared-structure path morphs. The two states share strokes, and the rest reshape. Play folds into pause, a plus swings into a check, a sun's rays retract as the moon's bite sweeps in.
- Sequenced choreography. One move after another: the menu's outer bars converge before they cross, the phone's signal arcs retract before the handset turns to hang up, an upload arrow launches before its cloud seals.
- Masked slashes. For off states, a slash is drawn through the icon and a matching gap is cut behind it with a mask, so the strokes never collide: the eye, the microphone, the bell and the camera.
Success and arrival states, such as saved, added, invited and verified, land with the settle spring, the one overshoot Youj allows.
Reversal
Turning a state off plays the opposite choreography, not the same one backwards: the menu's bars uncross before they separate. Toggle again mid-morph and the icon turns back from wherever it is, without a jump. Set the preview to ¼ speed and interrupt the morph to watch it.
Driving them from state
A state icon follows your state; it doesn't hold its own. That keeps it honest when the state changes from somewhere else, like a keyboard shortcut or the server. In this editor, each toolbar button is a pressed toggle, and its icon shows the formatting it applies.
useIconState builds the state, the button props and the icon props in one call:
import { BoldIcon, useIconState } from '@youj/icons'
const bold = useIconState({ label: 'Bold' })
<button {...bold.buttonProps}>
<BoldIcon {...bold.iconProps} />
</button>Reference
Propssize, strokeWidth, aria-label… (8)
Props
State icons take the same props as every icon. active is the one that matters here: it overrides the trigger and holds the icon in its second state while it's true.
| Prop | Type | |
|---|---|---|
size | number | stringRendered width and height. default 24 | 24 |
strokeWidth | numberStroke width in viewBox units. default 1.5 | 1.5 |
className | string | – |
style | CSSProperties | – |
aria-label | stringGives the icon role="img". Without it the icon is decorative (aria-hidden). | – |
trigger | IconTrigger'hover''mount''loop''none'What plays the choreography. Hover also listens to a hovered/focused parent button or | – |
active | booleanControlled state for state icons (menu → close, play → pause…). Overrides the trigger. | – |
ref | Ref<SVGSVGElement> | – |
MotionMorph steps, second beat, snaps and flips… · reduced motion
Motion
| Moment | Tokens | ||
|---|---|---|---|
| Morph stepseach move in a sequence | duration.fastease.standard140ms · cubic-bezier(0.2, 0, 0, 1) | 140mscubic-bezier(0.2, 0, 0, 1) | |
Second beatstarts one fast step after the first | duration.baseease.standard220ms · cubic-bezier(0.2, 0, 0, 1) | 220mscubic-bezier(0.2, 0, 0, 1) | |
| Snaps and flipsparts that turn or click into place | spring.snappy0.2s, bounce 0 | 0.2s, bounce 0 | |
| Success and arrivalsaved, added, invited, verified | spring.settle0.45s, bounce 0.3 | 0.45s, bounce 0.3 | |
| Draw onchecks, slashes and marks writing themselves | duration.slowease.emphasized340ms · cubic-bezier(0.6, 0, 0.1, 1) | 340mscubic-bezier(0.6, 0, 0.1, 1) |
Reduced motion
The icon switches straight to its new state, with no morph.
Each icon's choreography is its own, built from these tokens. The explorer's Morph view describes what each one does, and toggles them all in a wave.
Reduced motion
The icon switches to its new state instantly. The state is still visible, since the slash, the pause bars or the close mark all appear, just without the morph.
AccessibilityKeyboard, ARIA, announcements
Accessibility
- A state icon is decorative. It shows the state; it doesn't announce it. Put the state on the control:
aria-pressedfor a toggle with one name (mute, bold, save), a name that changes for an action that flips (play and pause), oraria-expandedfor a disclosure (menu). - Keep the control's name in step with what the icon shows. A button whose icon shows "close" should be named for closing, or carry
aria-expanded="true". - Toggling from the keyboard morphs the icon the same way as a click.