Liquid✨ Premium

Icon Morph

The three-bar menu button that turns into a cross on click: the top and bottom bars slide to the center and rotate to ±45°, the middle one fades out, all in 0.4 s with a bounce and a shift from violet to pink. A real button, usable from the keyboard, that says whether it is open or closed; the cross appears without movement when the user reduces motion.

CSSJSInteraction

Updated

This effect is part of Effect.Labs — 840 vanilla effects, some free, some premium. Liquid has 31 effects, including 6 free. Explore the category →

3 usage examples

Menu of the interactive map of a regional agricultural show — Icon Morph example 1

① Menu of the interactive map of a regional agricultural show

WhenOn their phone, the visitor checks the hall map; they tap the button at the top right, it turns into a cross, and the Cattle, Sheep, Contests, Food list opens over the map.
WhyThe cross born from the three bars shows that the same button closes the menu: standing in the aisles, the visitor does not have to look for another button.
Settings50 px button in the app's dark green bar; cream bars #ecfccb, amber cross #fbbf24; 178 px white panel that opens when aria-expanded becomes true; 250 × 332 px phone screen.
Layers panel of a bike lane map — a metro area cycling app — Icon Morph example 2

② Layers panel of a bike lane map — a metro area cycling app

WhenFull-screen map; the button at the top left opens the layers to check: separated bike paths, painted bike lanes, bike parking, repair workshops.
WhyThe change is short (0.4 s) and in place: the map stays visible, the icon only says "open" or "closed".
SettingsWhite button with a gray border #c7d2c3, dark green bars #166534, red cross #dc2626; aria-label="Layers"; 226 px panel under the button, boxes checked in green.
Menu button of a digital comics app — Icon Morph example 3

③ Menu button of a digital comics app

WhenThe page is full screen; the discreet corner icon turns into a cross and opens the chapter list, with the current chapter highlighted.
WhyA 50 px icon that transforms in place is the least interface you can put over a comic page you do not want to cover.
SettingsOriginal colors (violet bars, pink cross) on a dark button background at 72%; aria-label="Chapters"; 230 px dark panel, current chapter on a violet background at 18%.

How it works

The .icon-morph button is a real 50 × 50 px <button type="button"> (violet background at 15%, 2 px border, 12 px corners) holding three empty <span> elements: the bars, 26 × 3 px, set in position: absolute at 15, 23 and 31 px from the top. When the script adds the .active class, the first and third bars move to the center (top: 23px) while rotating to 45° and −45°, and change from #a855f7 to #ec4899; the middle one fades out (opacity: 0 and scaleX(0)). A second click removes the class and brings the three bars back.

Everything runs through a single transition: transition: .4s cubic-bezier(.68, -.55, .27, 1.55) on the bars. The two values outside 0-1 in the curve make the bars back up slightly at the start, then overshoot their end position before settling: that bounce is what makes the icon feel elastic rather than a plain fade. The ten-line script walks through every .fx-0402-root .icon-morph on the page, toggles .active on click and keeps aria-expanded up to date, so the effect can be pasted several times.

Why a <button> rather than a clickable block: it gets focus with Tab, activates with Enter and Space without a line of code, and screen readers announce it as a button. Its name stays "Menu" (aria-label): aria-expanded is what says "collapsed" or "expanded". Also changing the label to "Close" would announce the state twice. The button only shows the state: your page opens the panel, by listening to the click or reading aria-expanded, as the three examples do.

Accessibility

  • Keyboard: measured in Chromium 151 and Firefox 153, Tab reaches the button, and Enter or Space opens then closes it; the pink focus ring (:focus-visible, 2 px, 3 px offset) only shows for keyboard users. In Safari, Tab skips buttons unless "Press Tab to highlight each item" is turned on (Option+Tab reaches them): that is a browser setting, not the effect.
  • Screen readers: the button is announced as "Menu, button, collapsed" then "expanded". Link it to your panel with aria-controls="panel-id", and give the panel real list or navigation content.
  • Reduced motion: under prefers-reduced-motion: reduce, the bars lose their position and rotation transitions and keep only color and opacity over 0.3 s. Measured in all three engines: the cross is in place from the first frame, and no frame changes after 300 ms.
  • Target size: 50 × 50 px, above the 44 px recommended for a finger. Do not go below that on mobile.

Browser compatibility

CSS (transitions on top, transform, opacity) + 0.5 KB of JavaScript, no dependencies. Measured frame by frame in Chromium 151, Firefox 153 and WebKit 26: same bar paths, same bounce, same stop under reduced motion. :focus-visible exists since Chrome 86, Firefox 85 and Safari 15.4; before that, the browser shows its usual focus outline.

Chrome 60+✓ Full
Firefox 60+✓ Full
Safari 12+✓ Full (Tab depends on the keyboard setting)
Edge 79+✓ Full
Mobile iOS 12+✓ Full (touch)
Android Chrome 60+✓ Full (touch)

Without JavaScript, the button keeps its three bars and never changes state: make sure your menu can be reached another way (a link to a navigation page, for instance). A browser without CSS transitions switches straight from bars to cross.

The code

HTML structure to paste into your page (the CSS + JS come with your purchase):

index.html — structure
<div class="fx-0402-root">
  <div class="icon-morph-container">
    <button type="button" class="icon-morph" aria-expanded="false" aria-label="Menu">
      <span></span><span></span><span></span>
    </button>
    <span class="icon-morph-label">…</span>
  </div>
</div>

One-time payment, no subscription — the code is yours forever

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
Colors (CSS — background of .icon-morph span and of the .active rules) bars #a855f7, cross #ec4899 Use your navigation bar color for the bars and a soft alert color for the cross. Keep a contrast of at least 3:1 against the button background.
Duration and bounce (CSS — transition: .4s cubic-bezier(.68, -.55, .27, 1.55)) 0.4 s, back-up then overshoot .3s ease gives a cross without bounce, more restrained; beyond 0.6 s, the button feels slow to respond.
Size (CSS — button width / height, bar top) 50 px button, 26 × 3 px bars at 15 / 23 / 31 px For a 40 px button: 22 px bars at 11 / 18 / 25 px, and top: 18px in both .active rules. The gaps between bars must stay equal for the cross to meet at the center.
Accessible name (HTML — aria-label) "Menu" Name what the button opens: "Layers", "Chapters", "Filters". Add aria-controls with your panel's id.
Middle bar (CSS — .active span:nth-child(2)) opacity: 0 and scaleX(0) transform: translateX(-20px) instead of scaleX(0) makes it slide out to the left instead of shrinking at the center.

FAQ

The button only shows the state. Listen to its click and read aria-expanded: btn.addEventListener('click', function () { panel.hidden = btn.getAttribute('aria-expanded') !== 'true'; });. Put this code after the effect's script, which updates aria-expanded first. Also close the menu with Escape and give focus back to the button.
Because aria-expanded already carries the state: a screen reader says "Menu, button, expanded". Also renaming it "Close menu" would repeat the information, and some readers do not announce a name change on an element that already has focus. A fixed name and a changing state is the recommended pattern for a button that opens and closes.
The top property recalculates layout on every frame, while transform only moves a layer that is already painted. Here it applies to three 26 × 3 px bars inside a 50 px button, for 0.4 s: the cost is negligible, even on an entry-level phone. If you animate dozens of buttons at once, replace the top values in the .active rules with a translateY(8px) and translateY(-8px) placed before the rotation.