Hamburger to X
Tap the menu icon and its three bars morph into a close icon: the outer bars slide together, the middle one shrinks away, and they swing round into an X with a little spring. It’s a real toggle button with aria-expanded.
Animacious
How to use it
- Paste the snippet just before
</head>. - Add an empty
<button data-hamburger-to-x aria-label="Menu"></button>. Size it withfont-size, colour it withcolor. - Open and close your menu when it fires its
toggleevent.
Options
Add any of these attributes to the same element.
data-open | Start as an X. |
data-autoplay | It toggles itself every couple of seconds while in view. Handy for demos. |
The toggle event tells you which way it went: e.detail.open. From code, animacious.hamburger(button) toggles it.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Hamburger to X: tap the menu icon and its three bars morph into a close
// icon. The outer bars slide together, the middle one shrinks away, then
// they swing round into an X with a little spring. Tap again to go back.
// It's a real toggle: aria-expanded follows it.
// Usage: <button data-hamburger-to-x aria-label="Menu" aria-controls="nav"></button>
// Size it with font-size, colour it with color.
// Optional: data-open (start as an X), data-autoplay (it toggles itself
// every couple of seconds while in view; handy for demos).
// Listen for the "toggle" event: e.detail.open is true or false.
// Script use: animacious.hamburger(button) toggles it from code.
var DEFAULTS = { every: 1800 };
var GAP = 0.32; // em between bars
var css =
'.htx{position:relative;display:inline-grid;place-items:center;width:1.8em;height:1.8em;padding:0;border:0;background:none;color:inherit;cursor:pointer;-webkit-tap-highlight-color:transparent}' +
'.htx-bar{position:absolute;left:50%;top:50%;width:1.1em;height:0.12em;margin:-0.06em 0 0 -0.55em;border-radius:0.06em;background:currentColor;pointer-events:none}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function place(s, open) {
anime.utils.set(s.bars[0], { y: open ? '0em' : -GAP + 'em', rotate: open ? 45 : 0 });
anime.utils.set(s.bars[1], { scaleX: open ? 0 : 1, opacity: open ? 0 : 1 });
anime.utils.set(s.bars[2], { y: open ? '0em' : GAP + 'em', rotate: open ? -45 : 0 });
}
function setup(btn) {
btn.classList.add('htx');
if (btn.tagName === 'BUTTON' && !btn.getAttribute('type')) btn.type = 'button';
if (!btn.getAttribute('aria-label')) btn.setAttribute('aria-label', 'Menu');
btn.textContent = '';
var bars = [];
for (var i = 0; i < 3; i++) {
var bar = document.createElement('span');
bar.className = 'htx-bar';
bar.setAttribute('aria-hidden', 'true');
btn.appendChild(bar);
bars.push(bar);
}
var open = btn.dataset.open !== undefined && btn.dataset.open !== 'false';
btn._htx = { bars: bars, open: open };
btn.setAttribute('aria-expanded', open);
place(btn._htx, open);
}
function toggle(btn) {
var s = btn._htx;
var open = (s.open = !s.open);
btn.setAttribute('aria-expanded', open);
btn.dispatchEvent(new CustomEvent('toggle', { bubbles: true, detail: { open: open } }));
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { place(s, open); return; }
var top = s.bars[0], mid = s.bars[1], bottom = s.bars[2];
if (open) {
// Slide together, then swing round into an X.
anime.animate([top, bottom], { y: '0em', duration: 180, ease: 'in(2)' });
anime.animate(mid, { scaleX: 0, opacity: 0, duration: 180, ease: 'in(2)' });
anime.animate(top, { rotate: 45, duration: 700, delay: 170, ease: 'outElastic(1, .55)' });
anime.animate(bottom, { rotate: -45, duration: 700, delay: 170, ease: 'outElastic(1, .55)' });
} else {
// Unswing first, then spread back out, with the middle bar returning.
anime.animate([top, bottom], { rotate: 0, duration: 200, ease: 'in(2)' });
anime.animate(top, { y: -GAP + 'em', duration: 600, delay: 190, ease: 'outElastic(1, .6)' });
anime.animate(bottom, { y: GAP + 'em', duration: 600, delay: 190, ease: 'outElastic(1, .6)' });
anime.animate(mid, { scaleX: [0, 1], opacity: 1, duration: 450, delay: 220, ease: 'out(3)' });
}
}
function init() {
document.querySelectorAll('[data-hamburger-to-x]').forEach(function (btn) {
if (btn._htx) return;
setup(btn);
btn.addEventListener('click', function () { toggle(btn); });
if (btn.dataset.autoplay !== undefined && btn.dataset.autoplay !== 'false' &&
!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
var timer = null;
var tick = function () {
if (!btn.matches(':hover') && document.activeElement !== btn) toggle(btn);
timer = setTimeout(tick, DEFAULTS.every);
};
new IntersectionObserver(function (entries) {
clearTimeout(timer);
if (entries[0].isIntersecting) timer = setTimeout(tick, 600);
}).observe(btn);
}
});
}
window.animacious = window.animacious || {};
window.animacious.hamburger = function (btn) { if (btn._htx) toggle(btn); };
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><button id="menu-btn" data-hamburger-to-x aria-label="Menu" aria-controls="nav"></button>
<script>
document.getElementById("menu-btn").addEventListener("toggle", (e) => {
document.getElementById("nav").hidden = !e.detail.open;
});
</script>