← All articles

State Icons: Menu to Close, Play to Pause, on a Spring

motion-state-icon has since been renamed to motion-icon-state. The examples below use the new name. The old name still works until version 2.0.

Many icons show a state. A menu button shows three lines when the menu is closed and a cross when it is open. A play button turns into a pause button. A copy button shows a check after it copies. <motion-icon-state>, new in motion-components 1.1, handles this change for you. The icon morphs from one state to the other on a spring, and it can turn around in the middle of a change.

This article explains how the element works and gives five complete recipes that you can paste into a page.

Why morph instead of swap

The usual way to show a state is to swap one icon for another. The menu icon disappears and a close icon appears in its place. This works, but the change is sudden, and the user does not see how the two states connect.

A morph moves the parts of one icon into the other. The three lines of the menu icon turn and meet in the middle to form a cross. The user sees one control that changes, not two different controls.

The morph also handles fast clicks. Every change runs on a spring from the current position. If a user clicks again before the change finishes, the icon turns around from where it is. It does not jump to the end first, and it does not wait.

Try it in one file

Save this as index.html and open it in a browser:

Click each icon to switch its state. You can also press Tab to move to an icon and press Space or Enter.

Eight icons

Set name to choose the icon. Set active to show the second state.

nameWithout activeWith activeTypical use
menumenucloseNavigation drawer
playplaypauseAudio and video players
copycopycheckCopy to clipboard
plusplusminusExpand and collapse
chevronchevron downchevron upAccordions and drop-down menus
heartempty heartfilled heartLike and favorite
loadingspinnercheckSave and submit
eyeeyeeye with a line through itShow and hide a password

All eight icons use a 24 pixel grid and 2 pixel round lines. This is the same style as Lucide, Tabler and Heroicons, so a state icon looks right next to the icons you already use.

Two ways to switch

1. The icon is the button

Add toggle and a label. The icon becomes a button. It switches itself on a click, or on the Space or Enter key, and it fires a motion-change event:

Use this way for small controls that have no text, such as a like icon on a card.

2. Your button controls the icon

Leave out toggle. Put the icon inside your own <button>, and set active from your code. Your button handles the click, the keyboard and the accessible name. The icon only shows the state.

Use this way when the button already exists, or when it has text next to the icon. All five recipes below work this way.

Recipe 1: Menu button

The button tells screen readers whether the menu is open with aria-expanded. The icon follows the same value.

Recipe 2: Copy button

The icon shows a check for 1.5 seconds after the copy, then returns to the copy icon.

Recipe 3: Save button with a spinner

The loading icon spins while it is not active. When you set active, the spinner stops and a check draws in. Start with active so the button shows a check before the first save.

Recipe 4: Show password

When the password is visible, the icon shows the eye with a line through it. The button label changes too, so a screen reader always says what the next click does.

Recipe 5: Accordion

The native <details> element fires a toggle event when it opens or closes. The chevron follows its open value. You need no extra button and no extra state.

Color, size and feel

PropertyWhat it changesDefault
--mc-icon-colorThe color of the linesThe text color
--mc-icon-color-activeThe icon in its second state, including the filled heart and the checks in copy and loading--mc-icon-color
--mc-icon-sizeThe width and height1.5em

Two attributes change how the morph feels:

AttributeWhat it changesDefault
durationHow long the morph takes, in seconds0.45
bounceHow much the spring overshoots. 0 means no overshoot.0.3

A higher bounce feels more playful. A lower bounce feels calmer. For a menu button in a serious interface, try bounce="0.1".

React and Vue

React 19 sets active as a property, so you can pass a boolean from state:

In Vue, bind :active after you tell the compiler that motion-* tags are custom elements. The framework setup guide shows the configuration.

Accessibility

SetupWhat a screen reader getsWhat you add
With toggleA button with aria-pressedA label
Inside your own buttonNothing from the icon. It is decorative.A name on the button: its text or aria-label
Alone, without toggleNothing. It is decorative.A label if the icon carries meaning

When a user turns on reduced motion in their system settings, the icon changes state at once with no morph. The loading spinner stands still.

Next steps

Read the motion-icon-state docs for live versions of every recipe in this article. For icons that do not show a state, such as a bell that wiggles or a heart that draws itself in, read motion-components 1.1: Animate Any SVG Icon With One HTML Tag.