I’ll build this as a strong standalone mobile-menu chunk for RX Theme: accessible drawer, dropdown submenus, focus trap, ESC close, overlay, scroll lock, resize handling, reduced-motion support, and safe WordPress-friendly initialization.
Below is a complete advanced-level code for:
assets/static-js/static-chunks-js/chunk-003-mobile-menu.js
It is written as a safe standalone JavaScript chunk for RX Theme. It does not require jQuery. It supports accessibility, mobile drawer menu, submenu toggles, overlay, body scroll lock, ESC close, click-outside close, focus trap, keyboard navigation, active menu detection, resize reset, and WordPress admin-bar compatibility.
/*!
* RX Theme - Mobile Menu Chunk
* File: assets/static-js/static-chunks-js/chunk-003-mobile-menu.js
* Version: 1.0.0
* Author: RxHarun
*
* Purpose:
* Advanced mobile menu functionality for RX Theme.
*
* Features:
* - Mobile drawer open/close
* - Overlay support
* - Body scroll lock
* - ESC key close
* - Click outside close
* - Focus trap for accessibility
* - ARIA attributes
* - Submenu accordion
* - Keyboard navigation
* - Current page active detection
* - Resize reset for desktop
* - WordPress admin bar offset support
* - Reduced motion support
* - Safe multiple-init protection
*/
(function () {
'use strict';
/**
* Prevent double initialization.
*/
if (window.RX_MOBILE_MENU_INITIALIZED) {
return;
}
window.RX_MOBILE_MENU_INITIALIZED = true;
/**
* Main RX namespace.
*/
window.RXTheme = window.RXTheme || {};
/**
* Mobile menu module.
*/
window.RXTheme.MobileMenu = (function () {
/**
* Default settings.
*/
var settings = {
breakpoint: 1024,
bodyOpenClass: 'rx-mobile-menu-open',
bodyLockClass: 'rx-scroll-locked',
menuOpenClass: 'is-open',
overlayOpenClass: 'is-active',
submenuOpenClass: 'is-submenu-open',
submenuParentClass: 'menu-item-has-children',
currentClass: 'rx-current-menu-item',
initializedClass: 'rx-mobile-menu-ready',
animationDuration: 300,
focusableSelectors: [
'a[href]',
'button:not([disabled])',
'input:not([disabled])',
'select:not([disabled])',
'textarea:not([disabled])',
'[tabindex]:not([tabindex="-1"])'
].join(',')
};
/**
* DOM cache.
*/
var dom = {
html: document.documentElement,
body: document.body,
toggle: null,
menu: null,
overlay: null,
closeButtons: [],
submenuToggles: [],
firstFocusable: null,
lastFocusable: null
};
/**
* State.
*/
var state = {
isOpen: false,
lastFocusedElement: null,
scrollY: 0,
resizeTimer: null,
prefersReducedMotion: false
};
/**
* Select first matching element from multiple selectors.
*/
function selectOne(selectors) {
for (var i = 0; i < selectors.length; i++) {
var element = document.querySelector(selectors[i]);
if (element) {
return element;
}
}
return null;
}
/**
* Select all matching elements from multiple selectors.
*/
function selectAll(selectors) {
var results = [];
selectors.forEach(function (selector) {
var nodes = document.querySelectorAll(selector);
nodes.forEach(function (node) {
if (results.indexOf(node) === -1) {
results.push(node);
}
});
});
return results;
}
/**
* Check if current screen is mobile.
*/
function isMobileView() {
return window.innerWidth < settings.breakpoint;
}
/**
* Check reduced motion preference.
*/
function detectReducedMotion() {
if (!window.matchMedia) {
return false;
}
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}
/**
* Get WordPress admin bar height.
*/
function getAdminBarHeight() {
var adminBar = document.getElementById('wpadminbar');
if (!adminBar) {
return 0;
}
return adminBar.offsetHeight || 0;
}
/**
* Apply admin bar offset to mobile menu.
*/
function applyAdminBarOffset() {
if (!dom.menu) {
return;
}
var offset = getAdminBarHeight();
if (offset > 0) {
dom.menu.style.setProperty('--rx-admin-bar-offset', offset + 'px');
} else {
dom.menu.style.removeProperty('--rx-admin-bar-offset');
}
}
/**
* Return all focusable elements inside menu.
*/
function getFocusableElements() {
if (!dom.menu) {
return [];
}
var elements = Array.prototype.slice.call(
dom.menu.querySelectorAll(settings.focusableSelectors)
);
return elements.filter(function (element) {
return (
element.offsetWidth > 0 ||
element.offsetHeight > 0 ||
element === document.activeElement
);
});
}
/**
* Update first and last focusable elements.
*/
function updateFocusableElements() {
var focusable = getFocusableElements();
dom.firstFocusable = focusable[0] || null;
dom.lastFocusable = focusable[focusable.length - 1] || null;
}
/**
* Safely focus an element.
*/
function safeFocus(element) {
if (!element || typeof element.focus !== 'function') {
return;
}
try {
element.focus({ preventScroll: true });
} catch (error) {
element.focus();
}
}
/**
* Lock body scroll.
*/
function lockScroll() {
state.scrollY = window.scrollY || window.pageYOffset || 0;
dom.body.classList.add(settings.bodyLockClass);
dom.body.style.position = 'fixed';
dom.body.style.top = '-' + state.scrollY + 'px';
dom.body.style.left = '0';
dom.body.style.right = '0';
dom.body.style.width = '100%';
}
/**
* Unlock body scroll.
*/
function unlockScroll() {
dom.body.classList.remove(settings.bodyLockClass);
dom.body.style.position = '';
dom.body.style.top = '';
dom.body.style.left = '';
dom.body.style.right = '';
dom.body.style.width = '';
window.scrollTo(0, state.scrollY || 0);
}
/**
* Set ARIA states.
*/
function setMenuAria(isOpen) {
if (dom.toggle) {
dom.toggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
}
if (dom.menu) {
dom.menu.setAttribute('aria-hidden', isOpen ? 'false' : 'true');
}
if (dom.overlay) {
dom.overlay.setAttribute('aria-hidden', isOpen ? 'false' : 'true');
}
}
/**
* Open mobile menu.
*/
function openMenu() {
if (state.isOpen || !dom.menu) {
return;
}
state.isOpen = true;
state.lastFocusedElement = document.activeElement;
applyAdminBarOffset();
updateFocusableElements();
dom.body.classList.add(settings.bodyOpenClass);
dom.menu.classList.add(settings.menuOpenClass);
if (dom.overlay) {
dom.overlay.classList.add(settings.overlayOpenClass);
}
setMenuAria(true);
lockScroll();
window.setTimeout(function () {
updateFocusableElements();
if (dom.firstFocusable) {
safeFocus(dom.firstFocusable);
} else {
safeFocus(dom.menu);
}
}, state.prefersReducedMotion ? 0 : 50);
document.dispatchEvent(
new CustomEvent('rxMobileMenuOpened', {
detail: {
menu: dom.menu
}
})
);
}
/**
* Close mobile menu.
*/
function closeMenu() {
if (!state.isOpen || !dom.menu) {
return;
}
state.isOpen = false;
dom.body.classList.remove(settings.bodyOpenClass);
dom.menu.classList.remove(settings.menuOpenClass);
if (dom.overlay) {
dom.overlay.classList.remove(settings.overlayOpenClass);
}
setMenuAria(false);
unlockScroll();
if (state.lastFocusedElement) {
safeFocus(state.lastFocusedElement);
} else if (dom.toggle) {
safeFocus(dom.toggle);
}
document.dispatchEvent(
new CustomEvent('rxMobileMenuClosed', {
detail: {
menu: dom.menu
}
})
);
}
/**
* Toggle menu.
*/
function toggleMenu(event) {
if (event) {
event.preventDefault();
}
if (state.isOpen) {
closeMenu();
} else {
openMenu();
}
}
/**
* Close all submenus.
*/
function closeAllSubmenus(exceptToggle) {
dom.submenuToggles.forEach(function (toggle) {
if (exceptToggle && toggle === exceptToggle) {
return;
}
var parent = toggle.closest('.' + settings.submenuParentClass);
var submenu = getSubmenuFromToggle(toggle);
toggle.setAttribute('aria-expanded', 'false');
if (parent) {
parent.classList.remove(settings.submenuOpenClass);
}
if (submenu) {
submenu.hidden = true;
submenu.setAttribute('aria-hidden', 'true');
}
});
}
/**
* Get submenu from toggle button.
*/
function getSubmenuFromToggle(toggle) {
if (!toggle) {
return null;
}
var controlledId = toggle.getAttribute('aria-controls');
if (controlledId) {
return document.getElementById(controlledId);
}
var parent = toggle.closest('.' + settings.submenuParentClass);
if (!parent) {
return null;
}
return parent.querySelector(':scope > ul, :scope > .sub-menu, :scope > .children');
}
/**
* Toggle submenu.
*/
function toggleSubmenu(event) {
event.preventDefault();
var toggle = event.currentTarget;
var parent = toggle.closest('.' + settings.submenuParentClass);
var submenu = getSubmenuFromToggle(toggle);
if (!submenu || !parent) {
return;
}
var isExpanded = toggle.getAttribute('aria-expanded') === 'true';
if (!isExpanded) {
closeAllSubmenus(toggle);
}
toggle.setAttribute('aria-expanded', isExpanded ? 'false' : 'true');
parent.classList.toggle(settings.submenuOpenClass, !isExpanded);
submenu.hidden = isExpanded;
submenu.setAttribute('aria-hidden', isExpanded ? 'true' : 'false');
updateFocusableElements();
document.dispatchEvent(
new CustomEvent('rxMobileSubmenuToggled', {
detail: {
toggle: toggle,
submenu: submenu,
isOpen: !isExpanded
}
})
);
}
/**
* Create submenu toggle buttons if missing.
*/
function enhanceSubmenus() {
if (!dom.menu) {
return;
}
var parents = dom.menu.querySelectorAll(
'.' + settings.submenuParentClass + ', .page_item_has_children'
);
parents.forEach(function (parent, index) {
var submenu = parent.querySelector(':scope > ul, :scope > .sub-menu, :scope > .children');
if (!submenu) {
return;
}
var link = parent.querySelector(':scope > a');
var existingToggle = parent.querySelector(':scope > .rx-submenu-toggle');
if (!submenu.id) {
submenu.id = 'rx-mobile-submenu-' + index + '-' + Math.random().toString(36).slice(2, 8);
}
submenu.hidden = true;
submenu.setAttribute('aria-hidden', 'true');
if (existingToggle) {
existingToggle.setAttribute('aria-expanded', 'false');
existingToggle.setAttribute('aria-controls', submenu.id);
return;
}
var button = document.createElement('button');
button.type = 'button';
button.className = 'rx-submenu-toggle';
button.setAttribute('aria-expanded', 'false');
button.setAttribute('aria-controls', submenu.id);
var labelText = link ? link.textContent.trim() : 'submenu';
button.setAttribute('aria-label', 'Open submenu for ' + labelText);
button.innerHTML =
'<span class="rx-submenu-toggle-icon" aria-hidden="true"></span>' +
'<span class="screen-reader-text">Open submenu</span>';
if (link) {
link.insertAdjacentElement('afterend', button);
} else {
parent.insertBefore(button, submenu);
}
});
dom.submenuToggles = Array.prototype.slice.call(
dom.menu.querySelectorAll('.rx-submenu-toggle')
);
dom.submenuToggles.forEach(function (toggle) {
toggle.removeEventListener('click', toggleSubmenu);
toggle.addEventListener('click', toggleSubmenu);
});
}
/**
* Mark active/current links.
*/
function markCurrentLinks() {
if (!dom.menu) {
return;
}
var currentUrl = normalizeUrl(window.location.href);
var links = dom.menu.querySelectorAll('a[href]');
links.forEach(function (link) {
var linkUrl = normalizeUrl(link.href);
if (linkUrl === currentUrl) {
link.classList.add(settings.currentClass);
var parent = link.closest('li');
if (parent) {
parent.classList.add(settings.currentClass);
}
}
});
}
/**
* Normalize URL.
*/
function normalizeUrl(url) {
try {
var parsed = new URL(url, window.location.origin);
parsed.hash = '';
var finalUrl = parsed.href;
if (finalUrl.endsWith('/')) {
finalUrl = finalUrl.slice(0, -1);
}
return finalUrl;
} catch (error) {
return url;
}
}
/**
* Trap focus inside menu.
*/
function trapFocus(event) {
if (!state.isOpen || event.key !== 'Tab') {
return;
}
updateFocusableElements();
if (!dom.firstFocusable || !dom.lastFocusable) {
event.preventDefault();
safeFocus(dom.menu);
return;
}
if (event.shiftKey && document.activeElement === dom.firstFocusable) {
event.preventDefault();
safeFocus(dom.lastFocusable);
} else if (!event.shiftKey && document.activeElement === dom.lastFocusable) {
event.preventDefault();
safeFocus(dom.firstFocusable);
}
}
/**
* Handle keyboard events.
*/
function handleKeydown(event) {
if (event.key === 'Escape' && state.isOpen) {
closeMenu();
return;
}
trapFocus(event);
}
/**
* Handle overlay click.
*/
function handleOverlayClick(event) {
event.preventDefault();
closeMenu();
}
/**
* Handle outside click.
*/
function handleDocumentClick(event) {
if (!state.isOpen) {
return;
}
var target = event.target;
if (
dom.menu &&
!dom.menu.contains(target) &&
dom.toggle &&
!dom.toggle.contains(target)
) {
closeMenu();
}
}
/**
* Close menu when a normal menu link is clicked.
*/
function handleMenuLinkClick(event) {
var link = event.target.closest('a[href]');
if (!link || !dom.menu.contains(link)) {
return;
}
var href = link.getAttribute('href');
if (!href || href === '#' || href.indexOf('javascript:') === 0) {
return;
}
if (isMobileView()) {
closeMenu();
}
}
/**
* Handle resize.
*/
function handleResize() {
clearTimeout(state.resizeTimer);
state.resizeTimer = window.setTimeout(function () {
applyAdminBarOffset();
if (!isMobileView() && state.isOpen) {
closeMenu();
}
}, 150);
}
/**
* Setup menu ARIA attributes.
*/
function setupAria() {
if (!dom.toggle || !dom.menu) {
return;
}
if (!dom.menu.id) {
dom.menu.id = 'rx-mobile-menu';
}
dom.toggle.setAttribute('aria-controls', dom.menu.id);
dom.toggle.setAttribute('aria-expanded', 'false');
if (!dom.toggle.hasAttribute('aria-label')) {
dom.toggle.setAttribute('aria-label', 'Open mobile menu');
}
dom.menu.setAttribute('aria-hidden', 'true');
if (!dom.menu.hasAttribute('tabindex')) {
dom.menu.setAttribute('tabindex', '-1');
}
if (dom.overlay) {
dom.overlay.setAttribute('aria-hidden', 'true');
}
}
/**
* Bind events.
*/
function bindEvents() {
if (dom.toggle) {
dom.toggle.removeEventListener('click', toggleMenu);
dom.toggle.addEventListener('click', toggleMenu);
}
dom.closeButtons.forEach(function (button) {
button.removeEventListener('click', closeMenu);
button.addEventListener('click', closeMenu);
});
if (dom.overlay) {
dom.overlay.removeEventListener('click', handleOverlayClick);
dom.overlay.addEventListener('click', handleOverlayClick);
}
if (dom.menu) {
dom.menu.removeEventListener('click', handleMenuLinkClick);
dom.menu.addEventListener('click', handleMenuLinkClick);
}
document.removeEventListener('keydown', handleKeydown);
document.addEventListener('keydown', handleKeydown);
document.removeEventListener('click', handleDocumentClick);
document.addEventListener('click', handleDocumentClick);
window.removeEventListener('resize', handleResize);
window.addEventListener('resize', handleResize, { passive: true });
window.removeEventListener('orientationchange', handleResize);
window.addEventListener('orientationchange', handleResize, { passive: true });
}
/**
* Cache DOM elements.
*/
function cacheDom() {
dom.html = document.documentElement;
dom.body = document.body;
dom.toggle = selectOne([
'[data-rx-mobile-menu-toggle]',
'.rx-mobile-menu-toggle',
'.menu-toggle',
'.navbar-toggle'
]);
dom.menu = selectOne([
'[data-rx-mobile-menu]',
'.rx-mobile-menu',
'#rx-mobile-menu',
'.main-navigation',
'.site-navigation'
]);
dom.overlay = selectOne([
'[data-rx-mobile-menu-overlay]',
'.rx-mobile-menu-overlay',
'.rx-menu-overlay'
]);
dom.closeButtons = selectAll([
'[data-rx-mobile-menu-close]',
'.rx-mobile-menu-close',
'.rx-menu-close'
]);
}
/**
* Create overlay if theme does not provide one.
*/
function maybeCreateOverlay() {
if (dom.overlay || !dom.body) {
return;
}
var overlay = document.createElement('div');
overlay.className = 'rx-mobile-menu-overlay';
overlay.setAttribute('data-rx-mobile-menu-overlay', '');
overlay.setAttribute('aria-hidden', 'true');
dom.body.appendChild(overlay);
dom.overlay = overlay;
}
/**
* Public init.
*/
function init(customSettings) {
if (customSettings && typeof customSettings === 'object') {
Object.keys(customSettings).forEach(function (key) {
settings[key] = customSettings[key];
});
}
state.prefersReducedMotion = detectReducedMotion();
cacheDom();
if (!dom.toggle || !dom.menu) {
return;
}
maybeCreateOverlay();
setupAria();
enhanceSubmenus();
markCurrentLinks();
applyAdminBarOffset();
bindEvents();
dom.body.classList.add(settings.initializedClass);
document.dispatchEvent(
new CustomEvent('rxMobileMenuReady', {
detail: {
toggle: dom.toggle,
menu: dom.menu,
overlay: dom.overlay
}
})
);
}
/**
* Public destroy.
*/
function destroy() {
if (state.isOpen) {
closeMenu();
}
if (dom.toggle) {
dom.toggle.removeEventListener('click', toggleMenu);
}
dom.closeButtons.forEach(function (button) {
button.removeEventListener('click', closeMenu);
});
if (dom.overlay) {
dom.overlay.removeEventListener('click', handleOverlayClick);
}
if (dom.menu) {
dom.menu.removeEventListener('click', handleMenuLinkClick);
}
dom.submenuToggles.forEach(function (toggle) {
toggle.removeEventListener('click', toggleSubmenu);
});
document.removeEventListener('keydown', handleKeydown);
document.removeEventListener('click', handleDocumentClick);
window.removeEventListener('resize', handleResize);
window.removeEventListener('orientationchange', handleResize);
dom.body.classList.remove(settings.initializedClass);
}
return {
init: init,
open: openMenu,
close: closeMenu,
toggle: toggleMenu,
destroy: destroy
};
})();
/**
* Auto init on DOM ready.
*/
function domReady(callback) {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', callback);
} else {
callback();
}
}
domReady(function () {
window.RXTheme.MobileMenu.init();
});
})();
For this JavaScript to work beautifully, your theme HTML should have names like this:
<button class="rx-mobile-menu-toggle" type="button">
Menu
</button>
<nav id="rx-mobile-menu" class="rx-mobile-menu">
<button class="rx-mobile-menu-close" type="button">Close</button>
<ul>
<li><a href="/">Home</a></li>
<li class="menu-item-has-children">
<a href="/health/">Health</a>
<ul class="sub-menu">
<li><a href="/health/bone/">Bone Health</a></li>
<li><a href="/health/eye/">Eye Health</a></li>
</ul>
</li>
</ul>
</nav>
And add this basic CSS later in your mobile-menu CSS file:
.rx-mobile-menu {
position: fixed;
top: var(--rx-admin-bar-offset, 0);
left: 0;
bottom: 0;
width: min(88vw, 360px);
transform: translateX(-100%);
visibility: hidden;
overflow-y: auto;
z-index: 99999;
background: #fff;
transition: transform 0.3s ease, visibility 0.3s ease;
}
.rx-mobile-menu.is-open {
transform: translateX(0);
visibility: visible;
}
.rx-mobile-menu-overlay {
position: fixed;
inset: 0;
background: rgb(0 0 0 / 45%);
opacity: 0;
visibility: hidden;
z-index: 99998;
transition: opacity 0.3s ease, visibility 0.3s ease;
}
.rx-mobile-menu-overlay.is-active {
opacity: 1;
visibility: visible;
}
.rx-scroll-locked {
overflow: hidden;
}
.rx-submenu-toggle {
cursor: pointer;
}
.sub-menu[hidden],
.children[hidden] {
display: none;
}
@media (prefers-reduced-motion: reduce) {
.rx-mobile-menu,
.rx-mobile-menu-overlay {
transition: none;
}
}
Best enqueue name for this file:
wp_enqueue_script(
'rx-chunk-mobile-menu',
get_template_directory_uri() . '/assets/static-js/static-chunks-js/chunk-003-mobile-menu.js',
array(),
RX_THEME_VERSION,
true
);