Mobile-Friendly Multi-Level WordPress Menu with Custom Walker and jQuery

Creating a smooth, responsive mobile navigation that supports second- and third-level submenus can be tricky.
Below is a complete example using a custom Walker_Nav_Menu class, tailored HTML markup, CSS transitions, and a little jQuery to give users an elegant slide/accordion experience.

1. PHP: Custom Walker

Place this in your theme (e.g. in functions.php or a dedicated class-mobile-walker.php):

class Mobile_Walker_Nav_Menu extends Walker_Nav_Menu {

// Start <ul>
function start_lvl( &$output, $depth = 0, $args = null ) {
$indent = str_repeat("\t", $depth);
$classes = $depth === 0 ? 'submenu' : 'third-submenu';
$output .= "\n$indent<ul class=\"$classes\">\n";
}

// Start <li>
function start_el( &$output, $item, $depth = 0, $args = null, $id = 0 ) {
$indent = $depth ? str_repeat("\t", $depth) : '';
$classes = empty( $item->classes ) ? array() : (array) $item->classes;
$classes[] = 'nav-list__item';
$class_names = join( ' ', array_filter( $classes ) );

$has_children = in_array( 'menu-item-has-children', $classes, true );
$trigger_class = $has_children
? ( $depth === 0 ? ' submenu--trigger' : ' third-submenu--trigger' )
: '';

$output .= $indent . '<li class="' . esc_attr( $class_names . $trigger_class ) . '">';

$atts = array(
'href' => ! empty( $item->url ) ? $item->url : '',
'class' => 'nav-link',
);

$attributes = '';
foreach ( $atts as $attr => $value ) {
if ( ! empty( $value ) ) {
$value = ( 'href' === $attr ) ? esc_url( $value ) : esc_attr( $value );
$attributes .= ' ' . $attr . '="' . $value . '"';
}
}

$item_output = '<a' . $attributes . '>';
$item_output .= apply_filters( 'the_title', $item->title, $item->ID );
if ( $has_children ) {
$item_output .= '<i class="caret-down-icon"></i>';
}
$item_output .= '</a>';

$output .= $item_output;
}
}


2. Header Markup

Insert this in your header template where the mobile menu should appear:

<header class="new-mobile-header">
<div class="nav-brand">
<a href="<?php echo esc_url(home_url('/')); ?>">
<img src="<?php echo get_template_directory_uri(); ?>/images/logo.svg" class="mob-logo" alt="Logo">
</a>
</div>

<div class="nav-toggle">
<a class="btn--toggle nav-link" href="#" data-target="#collapse">
<i class="fa fa-bars fa-fw"></i>
</a>
</div>

<div class="nav-collapse" id="collapse">
<div class="mobile-pop-logo">
<div class="nav-brand">
<a href="<?php echo esc_url(home_url('/')); ?>">
<img src="<?php echo get_template_directory_uri(); ?>/images/logo.svg" class="mob-logo" alt="Logo">
</a>
</div>
<div class="nav-toggle">
<a class="btn--toggle nav-link" href="#" data-target="#collapse">
<img class="close-btn-menu" src="/wp-content/uploads/2025/09/close-x-svgrepo-com.svg" alt="Close">
</a>
</div>
</div>

<nav role="navigation">
<?php
wp_nav_menu( array(
'theme_location' => 'mobile-menu',
'container' => false,
'menu_class' => 'nav-list',
'walker' => new Mobile_Walker_Nav_Menu(),
) );
?>
</nav>
</div>
</header>

3. CSS Styling

Add to your theme’s main stylesheet or a custom CSS file:

ul#menu-mobile-menu li { font-family: Raleway, sans-serif; }

/* Mobile header styles */
header.new-mobile-header {
display: flex;
flex-flow: row wrap;
align-items: center;
justify-content: space-between;
color: #FFF;
background: #003e92;
transition: all 0.4s ease-out;
}

.nav-link { color: inherit; transition: all 0.4s; text-decoration: none; }
.nav-brand { padding: 0 16px; }

/* Caret indicators & submenu visibility */
.submenu--trigger .submenu,
.third-submenu--trigger .third-submenu {
visibility: visible;
opacity: 1;
}

.submenu--trigger i,
.third-submenu--trigger i {
position: absolute;
top: calc(50% - 6px);
right: 8px;
width: 15px;
height: 15px;
border-right: 2px solid #fff;
border-bottom: 2px solid #fff;
transform: rotate(45deg);
transition: transform 0.3s ease;
padding: 5px;
}

.submenu--trigger.active > a i,
.third-submenu--trigger.active a i {
transform: rotate(226deg);
}

/* Responsive menu behaviour */
@media (max-width: 991px) {
.nav-collapse {
display: flex;
flex-direction: column;
overflow-x: hidden;
overflow-y: auto;
max-height: 0;
transform: translateX(100%);
transition: transform .4s ease-out;
position: fixed;
right: 0;
top: 0;
min-height: 100vh;
background: #023e92;
z-index: 10;
}
.nav-collapse.showing { transform: translateX(0); }

.submenu,
.third-submenu {
transform: scaleY(0);
transform-origin: top;
overflow: hidden;
transition: transform 0.4s ease;
}
.submenu--trigger.active .submenu,
.third-submenu--trigger.active .third-submenu {
transform: scaleY(1);
}
}


4. jQuery Toggle Script

Enqueue this via your theme’s JS file:

jQuery(function ($) {
const $btnToggle = $('.btn--toggle');

// Open/close main mobile menu
$btnToggle.on('click', function () {
const $target = $($(this).attr('data-target'));
$target.toggleClass('showing');
});

// First-level submenu toggle
$('.submenu--trigger > a .caret-down-icon').on('click', function (e) {
e.preventDefault();
e.stopPropagation();
$(this).closest('.submenu--trigger').toggleClass('active');
});

// Second-level submenu toggle
$('.third-submenu--trigger > a .caret-down-icon').on('click', function (e) {
e.preventDefault();
e.stopPropagation();
$(this).closest('.third-submenu--trigger').toggleClass('active');
});
});


Wrap-Up

With the Mobile_Walker_Nav_Menu class and the CSS/JS above, you’ll have a clean, multi-level mobile menu that expands on caret click and keeps parent links functional.

💬 Need help implementing or customizing this menu? I’m just one message away—feel free to reach out and I’ll be happy to assist!