Limit Navigation Sub-Menu Rendering Levels in WordPress

The WordPress menu interface is a great way to manipulate navigation menus on your site. If you’re building a theme, however, you may find yourself wanting to limit the number of available sub-menu depths without removing the sub-menu items themselves.

You can style your way around it (styling .sub-menu .sub-menu to act differently than .sub-menu), you can use the wp_nav_menu function’s depth argument to limit the depth, or you can use a custom Walker to force the output to match.

However, if your situation is such that the styling won’t work well and you want to limit navigation sub-menu depth, but not remove any menu items on lower depths (this happens with the depth argument in wp_nav_menu), then the third option is the way to go: create a custom walker.

<?php
/**
* Limit the number of sub-menu levels outputted.
*
* Removes the ul wrapper on any sub-menus beyond the 2nd level.
*
* @author Joshua David Nelson, [email protected]
*/
// Add menu walker to primary menu
add_filter( 'wp_nav_menu_args', 'jdn_primary_menu_args' );
function jdn_primary_menu_args( $args ) {
if( isset( $args['theme_location'] ) && 'primary' == $args['theme_location'] ) { // change based on the location
$walker = new JDN_Primary_Nav_Walker;
$args['walker'] = $walker;
}
return $args;
}
// Custom Walker
class JDN_Primary_Nav_Walker extends Walker_Nav_Menu {
function start_el( &$output, $item, $depth = 0, $args = array(), $id = 0 ) {
// Remove 'menu-item-has-children' class from elements beyond the second depth level
if( $depth > 1 && isset( $item->classes ) && is_array( $item->classes ) && ( $key = array_search( 'menu-item-has-children', $item->classes ) ) !== false ) {
unset( $item->classes[ $key ] );
}
parent::start_el( $output, $item, $depth, $args );
}
// Starting of a sub-menu
function start_lvl( &$output, $depth = 0, $args = array() ) {
$indent = str_repeat("\t", $depth);
if( $depth < 2 ) {
$output .= "\n$indent<ul class='sub-menu'>\n"; // The default wrap
} elseif( $depth >= 2 ) {
$output .= "\n$indent\n"; // no sub menu wrap
}
}
// End of a sub-menu
function end_lvl( &$output, $depth = 0, $args = array() ) {
$indent = str_repeat("\t", $depth);
if( $depth < 2 ) {
$output .= "$indent</ul>\n"; // the default wrap end
} elseif( $depth >= 2 ) {
$output .= "\n$indent\n"; // no wrap
}
}
}

This removes all sub-menu wraps on sub-menus beyond the second level.

Featured image via Flickr CC, by Basheer Tome

Discussion

    • Krisna,

      The Walker is called in the jdn_primary_menu_args function. The key thing here is the name of the menu, which is typically theme-specific. In this example 'primary' is the name of the menu location this Walker is applied to in the theme. Change that accordingly for your theme.

      For the Walker:

      1. The start_el runs at the start of the list item. This this case, it’s looking at any item with a depth over 1 that has a class menu-item-has-children and removing that class.
      2. The start_lvl is where the sub-menu wrapper ul element is applied. The Walker adds this, like the typical default, but in this case only on any sub-menu with a depth under 2 (so only one level of sub-menus is allowed), otherwise it adds the normal $indent value without the sub-menu wrap.
      3. The end_lvl does something similiar, but it fires at the end and either adds the closing tag or the default $indent value.

      Hope that helps!

      Thanks,
      Joshua