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

Aspectv1.0v2.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.

HTML
<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>
Note the three empty <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.

CSS
/* ── 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.

JavaScript
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');
  }
});
The breakpoint is still 699px - same as v1.0. Change it to whatever suits your project by updating @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.

← Back to tuts index   |   Read MicroMenu v1.0