Skip to content

Instantly share code, notes, and snippets.

@KittenCodes
Last active June 1, 2026 07:41
Show Gist options
  • Select an option

  • Save KittenCodes/af61b16fe00f2e1111702952b055c0a3 to your computer and use it in GitHub Desktop.

Select an option

Save KittenCodes/af61b16fe00f2e1111702952b055c0a3 to your computer and use it in GitHub Desktop.
Multi-Level Mobile Menu

Multi-Level Mobile Menu

Add the PHP, CSS, and JavaScript as 3 separate snippets using either:


Usage

Add a Shortcode element to Oxygen and use:


[mobile_menu menu="Main Menu"]

Replace Main Menu with the name of your WordPress menu.

Example:

[mobile_menu menu="Primary"]

The menu supports unlimited nested dropdown levels.

.custom-mobile-menu-wrap {
position: relative;
}
.custom-mobile-menu-trigger,
.custom-mobile-menu-close {
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
border: 0;
background: transparent;
cursor: pointer;
}
.custom-mobile-menu-trigger svg,
.custom-mobile-menu-close svg {
width: 28px;
height: 28px;
stroke: currentColor;
stroke-width: 2;
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
}
.custom-mobile-menu-trigger .close-icon {
display: none;
}
.custom-mobile-menu-wrap.is-open .burger-icon {
display: none;
}
.custom-mobile-menu-wrap.is-open .close-icon {
display: block;
}
.custom-mobile-menu-panel {
position: fixed;
top: 0;
right: 0;
bottom: 0;
width: min(90vw, 420px);
height: 100vh;
background: #ffffff;
box-shadow: -16px 0 40px rgba(0, 0, 0, 0.16);
padding: 24px;
z-index: 9999;
overflow-y: auto;
opacity: 0;
transform: translateX(100%);
pointer-events: none;
transition: opacity 300ms ease, transform 300ms ease;
}
.custom-mobile-menu-wrap.is-open .custom-mobile-menu-panel {
opacity: 1;
transform: translateX(0);
pointer-events: auto;
}
.custom-mobile-menu-panel-header {
display: flex;
justify-content: flex-end;
margin-bottom: 24px;
}
.custom-mobile-menu-list {
list-style: none;
padding: 0;
margin: 0;
}
.custom-mobile-menu-item {
border-bottom: 1px solid #e5e5e5;
}
.custom-mobile-menu-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.custom-mobile-menu-row a {
display: block;
flex: 1;
padding: 14px 0;
color: inherit;
text-decoration: none;
}
.custom-submenu-toggle {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border: 0;
background: transparent;
cursor: pointer;
}
.custom-submenu-toggle svg {
width: 20px;
height: 20px;
stroke: currentColor;
stroke-width: 2;
fill: none;
transition: transform 300ms ease;
}
.custom-mobile-menu-item.is-open > .custom-mobile-menu-row .custom-submenu-toggle svg {
transform: rotate(180deg);
}
.custom-submenu {
display: grid;
grid-template-rows: 0fr;
padding-left: 16px;
overflow: hidden;
transition: grid-template-rows 300ms ease;
}
.custom-submenu > .custom-mobile-menu-list {
min-height: 0;
}
.custom-mobile-menu-item.is-open > .custom-submenu {
grid-template-rows: 1fr;
}
document.addEventListener('click', function (event) {
const trigger = event.target.closest('.custom-mobile-menu-trigger');
const close = event.target.closest('.custom-mobile-menu-close');
const submenuToggle = event.target.closest('.custom-submenu-toggle');
if (trigger) {
const wrapper = trigger.closest('.custom-mobile-menu-wrap');
const isOpen = wrapper.classList.toggle('is-open');
trigger.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
trigger.setAttribute('aria-label', isOpen ? 'Close menu' : 'Open menu');
return;
}
if (close) {
const wrapper = close.closest('.custom-mobile-menu-wrap');
const triggerButton = wrapper.querySelector('.custom-mobile-menu-trigger');
wrapper.classList.remove('is-open');
if (triggerButton) {
triggerButton.setAttribute('aria-expanded', 'false');
triggerButton.setAttribute('aria-label', 'Open menu');
}
return;
}
if (submenuToggle) {
const item = submenuToggle.closest('.custom-mobile-menu-item');
const isOpen = item.classList.toggle('is-open');
submenuToggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
}
});
<?php
add_shortcode('custom_mobile_menu', function ($atts) {
$atts = shortcode_atts([
'menu' => '',
], $atts);
$menu_items = wp_get_nav_menu_items($atts['menu']);
if (!$menu_items) {
return '';
}
$items_by_parent = [];
foreach ($menu_items as $item) {
$items_by_parent[(int) $item->menu_item_parent][] = $item;
}
$render_items = function ($parent_id) use (&$render_items, $items_by_parent) {
if (empty($items_by_parent[$parent_id])) {
return '';
}
$html = '<ul class="custom-mobile-menu-list">';
foreach ($items_by_parent[$parent_id] as $item) {
$has_children = !empty($items_by_parent[(int) $item->ID]);
$html .= '<li class="custom-mobile-menu-item' . ($has_children ? ' has-children' : '') . '">';
$html .= '<div class="custom-mobile-menu-row">';
$html .= '<a href="' . esc_url($item->url) . '">' . esc_html($item->title) . '</a>';
if ($has_children) {
$html .= '<button type="button" class="custom-submenu-toggle" aria-expanded="false" aria-label="Toggle submenu">';
$html .= '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg>';
$html .= '</button>';
}
$html .= '</div>';
if ($has_children) {
$html .= '<div class="custom-submenu">';
$html .= $render_items((int) $item->ID);
$html .= '</div>';
}
$html .= '</li>';
}
$html .= '</ul>';
return $html;
};
ob_start();
?>
<div class="custom-mobile-menu-wrap">
<button class="custom-mobile-menu-trigger" type="button" aria-expanded="false" aria-label="Open menu">
<svg class="burger-icon" viewBox="0 0 24 24" aria-hidden="true">
<path d="M4 6h16M4 12h16M4 18h16"/>
</svg>
<svg class="close-icon" viewBox="0 0 24 24" aria-hidden="true">
<path d="M6 6l12 12M18 6L6 18"/>
</svg>
</button>
<nav class="custom-mobile-menu-panel" aria-label="Mobile menu">
<div class="custom-mobile-menu-panel-header">
<button class="custom-mobile-menu-close" type="button" aria-label="Close menu">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M6 6l12 12M18 6L6 18"/>
</svg>
</button>
</div>
<?php echo $render_items(0); ?>
</nav>
</div>
<?php
return ob_get_clean();
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment