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.
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



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.
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):
<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 / property | Default | Effect |
|---|---|---|
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
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.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.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.