MicroMenu v2.0
"Still small. No longer ancient."
The original MicroMenu was a neat trick: a responsive nav in a handful of lines, with only 89 characters of JavaScript. It used a <select> dropdown to replace the menu on mobile, which was clever in 2013 - but the web has moved on.
MicroMenu 2.0 keeps the same philosophy - no jQuery, no frameworks, minimal code - but replaces the <select> with a proper animated hamburger menu using techniques that are now standard across browsers.
The nav bar above this text is the demo. Resize your browser below 699px (or open on a phone) to see it switch modes.
What changed and why
| Aspect | v1.0 | v2.0 |
|---|---|---|
| Mobile UI | <select> - triggers native OS picker |
Hamburger button + slide-down drawer |
| JavaScript | Inline onChange attribute |
addEventListener in a <script> block |
| Navigation | self.location (archaic) |
Standard <a href> links |
| Unnecessary markup | <form> wrapper around <select> |
Removed - it served no purpose |
| Accessibility | No keyboard support, no ARIA | aria-label, aria-expanded, aria-controls |
| CSP compatibility | Inline event handlers blocked by strict CSP | No inline handlers - CSP-safe |
The HTML
The structure is the same <nav>-wraps-everything approach. The <ul> is for desktop, and a <div> drawer (hidden by default) serves mobile. The hamburger is a <button> - not a <div>, not a checkbox hack - so it gets focus, keyboard activation, and screen-reader semantics for free.
<nav id="main-nav" aria-label="Main navigation">
<!-- Desktop: horizontal link list -->
<ul class="nav-links" role="list">
<li><a href="index.html" class="active">Home</a></li>
<li><a href="pages/side1.html">Page 1</a></li>
<li><a href="pages/side2.html">Page 2</a></li>
<li><a href="pages/side3.html">Page 3</a></li>
</ul>
<!-- Mobile: hamburger button -->
<button class="burger" id="burger-btn"
aria-label="Toggle menu"
aria-expanded="false"
aria-controls="nav-drawer">
<span></span>
<span></span>
<span></span>
</button>
<!-- Mobile: slide-down drawer -->
<div class="nav-drawer" id="nav-drawer"
role="navigation">
<a href="index.html" class="active">Home</a>
<a href="pages/side1.html">Page 1</a>
<a href="pages/side2.html">Page 2</a>
<a href="pages/side3.html">Page 3</a>
</div>
</nav>
<span> elements inside the button - these are the three bars of the hamburger icon, drawn entirely in CSS and animated into an ✕ when the menu opens.
The CSS
The show/hide logic is the same idea as v1.0: the desktop list and the mobile button are swapped at the breakpoint. The drawer uses a max-height transition for the slide-in effect — height: auto can't be transitioned directly in CSS, so max-height is the standard workaround.
/* ── Nav container ─────────────────────────── */
nav {
height: 52px;
display: flex;
align-items: center;
padding: 0 24px;
position: sticky;
top: 0;
}
/* ── Desktop links ─────────────────────────── */
.nav-links { display: flex; gap: 4px; list-style: none; }
.nav-links a {
padding: 6px 14px;
border-radius: 6px;
text-decoration: none;
transition: background .15s, color .15s;
}
/* ── Hamburger button ──────────────────────── */
.burger {
display: none; /* hidden on desktop */
flex-direction: column;
justify-content: center;
gap: 5px;
width: 36px; height: 36px;
border: none; background: none;
cursor: pointer;
}
.burger span {
display: block; height: 1.5px;
background: currentColor;
border-radius: 2px;
transform-origin: center;
transition: transform .25s ease, opacity .2s ease;
}
/* Animate bars → ✕ when nav has .open class */
nav.open .burger span:nth-child(1) { transform: translateY(6.5px) rotate(45deg); }
nav.open .burger span:nth-child(2) { opacity: 0; }
nav.open .burger span:nth-child(3) { transform: translateY(-6.5px) rotate(-45deg); }
/* ── Mobile drawer ─────────────────────────── */
.nav-drawer {
display: none; /* rendered but hidden (needed for transition) */
position: absolute;
top: 52px; left: 0; right: 0;
max-height: 0; /* collapsed by default */
overflow: hidden;
transition: max-height .28s ease, padding .28s ease;
}
nav.open .nav-drawer { max-height: 300px; padding: 8px 12px 12px; }
.nav-drawer a { display: block; padding: 12px 16px; border-radius: 8px; }
/* ── Responsive breakpoint ─────────────────── */
@media (max-width: 699px) {
.nav-links { display: none; } /* hide desktop links */
.burger { display: flex; } /* show burger button */
.nav-drawer{ display: block; } /* allow drawer to open */
}
The JavaScript
Three responsibilities: toggle the menu open/closed, close it when a link is tapped, and close it when the user clicks outside. That's it.
const nav = document.getElementById('main-nav');
const btn = document.getElementById('burger-btn');
// Toggle open/closed
btn.addEventListener('click', () => {
const open = nav.classList.toggle('open');
btn.setAttribute('aria-expanded', open);
});
// Close when a drawer link is tapped
document.getElementById('nav-drawer')
.querySelectorAll('a')
.forEach(a => {
a.addEventListener('click', () => {
nav.classList.remove('open');
btn.setAttribute('aria-expanded', 'false');
});
});
// Close on outside click
document.addEventListener('click', e => {
if (!nav.contains(e.target)) {
nav.classList.remove('open');
btn.setAttribute('aria-expanded', 'false');
}
});
@media (max-width: 699px) in the CSS.
Conclusion
MicroMenu 2.0 is still lightweight and dependency-free. The JavaScript is roughly 20 lines instead of 89 characters - but those 20 lines do a proper job: keyboard-accessible, screen-reader-friendly, CSP-safe, and without the native OS picker awkwardness of the old <select> approach.
As with v1.0: not suited for smooth-scrolled single-page sites - you'll want a scroll-spy setup for those. For everything else, it's still the smallest sensible RWD nav around.