The hamburger menu script provides a clean, accessible way to hide frequently used information while keeping it just one click away. This pattern works best for:
- Secondary navigation options
- Frequently Asked Questions (FAQ)
- Documentation and instructions
- Quick links that don’t warrant space in your primary navigation
Remember: Avoid hiding critical navigation or primary user paths in hamburger menus. For guidance on strategic implementation, see The Hamburger Menu Saga which explains why hamburgers work best for supplementary content rather than main navigation.
Implementation
First, place the hamburger script at the very end of your <body> element to ensure the DOM is fully loaded before the script runs:
<script src="assets/js/hamburger.js"></script>
</body>
⚠️ Performance tip: For production, download the script and host it on your own server rather than loading it from a CDN.
Second, structure your navigation. The script finds the panel by the rd-js-hamburger-nav hook class; presentational styling lives on a separate class (here .nav-hamburger). The panel should be hidden by default using CSS, and shown by the rd-is-active state class the script toggles:
.nav-hamburger {
position: fixed;
top: -100%;
width: 100%;
/* Additional styling as needed */
transition: top 0.3s ease;
}
.nav-hamburger.rd-is-active {
top: 0;
}
<div class="nav-hamburger rd-js-hamburger-nav">…</div>
Third, add the toggle button. The script hooks it via rd-js-hamburger; place it in your header or another accessible location:
<button class="hamburger no-decoration rd-js-hamburger" aria-label="Toggle navigation">
<span class="hamburger__icon-menu"></span>
<span class="hamburger__icon-close"></span>
</button>
The icon spans are empty on purpose: the glyphs are baked into the classes by CSS (Material Symbols ligatures rendered via :: before), so markup carries no vendor icon class and no ligature text. The menu ↔ close swap is also pure CSS keyed off the state class — the script never touches the icons. Rare Styles ships all of this; standalone users need:
.hamburger__icon-menu,
.hamburger__icon-close {
display: inline-block;
font-family: "Material Symbols Outlined";
}
.hamburger__icon-menu::before { content: "menu"; }
.hamburger__icon-close::before { content: "close"; }
.hamburger__icon-close { display: none; }
.hamburger.rd-is-active .hamburger__icon-menu { display: none; }
.hamburger.rd-is-active .hamburger__icon-close { display: inline-block; }
You can substitute any icon system or draw custom hamburger lines in CSS — the script only toggles state classes.
Contract
rd-js-hamburger/rd-js-hamburger-navare hooks: the script reads them, CSS never styles themrd-is-activeis state: the script writes it on both elements, CSS styles itaria-expandedon the button mirrors the open/closed state on every toggle; if the panel has anid, the script also setsaria-controlshamburger__icon-menu/hamburger__icon-closeare presentational BEM elements of the button — styled by CSS, invisible to the script- If either hook is missing on a page, the script safely does nothing
Functionality
The script automatically handles:
- Toggling the menu open/closed when clicking the hamburger button
- Closing the menu on a click outside the menu area or on Escape (both listeners are attached only while the menu is open)
- Adding/removing the
rd-is-activestate class and keepingaria-expandedin sync
Raw code
document.addEventListener('DOMContentLoaded', () => {
const hamburger = document.querySelector('.rd-js-hamburger');
const navGlobal = document.querySelector('.rd-js-hamburger-nav');
if (!hamburger || !navGlobal) return;
// Closing the menu when clicking outside — attached only while open
const outsideListener = (event) => {
if (!navGlobal.contains(event.target) && !hamburger.contains(event.target)) {
setState(false);
}
};
const escListener = (event) => {
if (event.key === 'Escape') setState(false);
};
const setState = (open) => {
hamburger.classList.toggle('rd-is-active', open);
navGlobal.classList.toggle('rd-is-active', open);
hamburger.setAttribute('aria-expanded', open ? 'true' : 'false');
if (open) {
document.addEventListener('click', outsideListener);
document.addEventListener('keydown', escListener);
} else {
document.removeEventListener('click', outsideListener);
document.removeEventListener('keydown', escListener);
}
};
if (navGlobal.id) hamburger.setAttribute('aria-controls', navGlobal.id);
setState(false);
// Toggle the hamburger menu on click
hamburger.addEventListener('click', () => {
setState(!navGlobal.classList.contains('rd-is-active'));
});
});
Changelog
v2.0.0
- Breaking: hooks moved to the
rd-jscontract — the script now finds elements viard-js-hamburger/rd-js-hamburger-nav, not.hamburger/.nav-hamburger - Breaking: state class renamed
.active→.rd-is-active - Breaking: icon elements renamed to BEM —
.icon-menu/.icon-close→.hamburger__icon-menu/.hamburger__icon-close(CSS-only rename; the script never reads them) - Added
aria-expandedon the toggle button, plusaria-controlswhen the panel carries anid - Added Escape to close; outside-click and Escape listeners are attached only while the menu is open
- Added a guard: pages without the hooks no longer throw