A menu often works only once when code compares inline styles while CSS supplies the real value. Keep the state in one class, let CSS control presentation, and read the boolean returned by classList.toggle().
Last updated: October 15, 2026.
<button id="menuButton" aria-controls="menuPanel" aria-expanded="false">
Menu
</button>
<nav id="menuPanel" class="menu-panel" hidden>...</nav>
<script>
const button = document.querySelector('#menuButton');
const panel = document.querySelector('#menuPanel');
button.addEventListener('click', () => {
const open = panel.classList.toggle('is-open');
panel.hidden = !open;
button.setAttribute('aria-expanded', String(open));
});
</script>Every click derives the next state from the current class. The same open value updates visibility and aria-expanded, so CSS, DOM state, and accessibility information do not drift apart.
Avoid mixing state mechanisms
MDN defines DOMTokenList.toggle() as adding a token when absent, removing it when present, and returning whether the token exists afterward. That return value is safer than checking strings such as element.style.display, which sees only inline declarations and may initially be empty.
Do not alternate between hidden, inline styles, classes, and separate booleans unless one clearly controls the others. One source of truth makes repeated clicks predictable.
Use a real button and expose the state
The W3C menu button pattern uses a button with aria-haspopup where appropriate and updates aria-expanded as the menu opens and closes. A native button already supports Enter and Space; a clickable div does not provide that behavior automatically.
If the panel contains site navigation rather than application commands, ordinary links inside a disclosed navigation region may be more suitable than ARIA menu roles. Close the panel on Escape and return focus to the opener when the interaction requires focus movement.
Continue with custom context menus, accessible tabs, and attribute-driven CSS state.