Building a Sub-menu: Display Parent Category/Page’s Children When Viewing Children

I would like a sub-menu in which the user visits a page, post, or category (in this example, “info”) and is then displayed a list of children categories/pages. Not a dropdown menu, but a menu that appears only when the user is already in the parent category.

I need a code which continues to display the parent’s children even when on that child page, post, or category. Maybe there is a way to do this with WP menus that I am unaware of?

Read More

The code needs to be separate from the main menu and be assigned the traditional CSS classes which indicate if the user is on the current page. The code also needs to automatically recognize the children of the parent, as opposed to assigning the children and calling multiple menus throughout the site. I’m positive this is possible because I used to utilize such a code. However, I can’t remember what that code was!

Below is an illustrated example of what such a menu may look like:
Classic menu example

Related posts

Leave a Reply

2 comments

  1. Funny, I just ran into this same conundrum a few months back…

    Background

    I found it quite bizarre that WordPress applies all manner of CSS classes to menu-items in order to distinguish the current item and its ancestors, but then completely ignores the sub-menus and their relationship with the current item. I wanted a .current-sub CSS class so that I could hide all .sub-menus except .current-subs.


    Theory

    After much research and wading through source, the solution that I ended up embracing was to extend the Walker_Nav_Menu class and modify its behavior at two distinct points in order to obtain my imagined .current-sub CSS class:

    1. When processing a menu-item ( see Walker_Nav_Menu->start_el() ), examine the classes that WordPress assigned to the menu-item in order to determine if it corresponds to the current page or is a menu ancestor of the current page.
    2. When processing a new menu-lvl (a.k.a “sub-menu”; see Walker_Nav_Menu->start_lvl() ), if the previous menu-item processed was either the current menu-item or an ancestor of it, apply a new class to the sub-menu element in order to distinguish it as a current submenu.

    Aside

    Note that I’ve emboldened menu ancestor above. In my situation, it turned out that I didn’t actually care about posts’, pages’, or categories’ hierarchy or relationships with one another (i.e. it didn’t matter whether or not page B was a child of page A). The only thing that I needed to worry about was the hierarchy of their corresponding menu-items.

    After the Walker was implemented, all I needed was a new call in a template file to wp_nav_menu() with an instance of my new menu Walker as an argument.


    Implementation

    For convenience and brevity I’ve provided my implementation in a procedural-style plugin. For the most part the code is identical to the native Walker_Nav_Menu – my comments denote my additions.

    //Register a new theme location for a custom menu
    function after_setup_theme_97226() {
        register_nav_menu( 'menu-97226', 'Primary Navigation (uses Menu_Walker_97226)' );
    }
    add_action( 'after_setup_theme', 'after_setup_theme_97226' );
    
    //Queue up CSS modifications
    function enqueue_styles_97226() {
        wp_enqueue_style( 'style-97226', plugins_url('style.css', __FILE__) );
    }
    add_action( 'wp_enqueue_scripts', 'enqueue_styles_97226' );
    
    //Now walk it out.
    class Menu_Walker_97226 extends Walker_Nav_Menu {
        const CURRENT_SUBMENU_CLASS = 'current-sub';    //The CSS class to apply to the active submenu
        var $is_current_submenu     = FALSE;            //Whether or not the next sub-menu to be processed is related to the current-menu-item.
    
        function start_lvl( &$output, $depth = 0, $args = array() ) {
            $indent = ( $depth ) ? str_repeat( "t", $depth ) : '';
    
            //If we've begun processing an "active" submenu, add a class to indicate as much.
            $current_sub_class = ( $this->is_current_submenu ) ? Menu_Walker_97226::CURRENT_SUBMENU_CLASS : '';
    
            //Add the new sub-menu ul element to the markup. Always give sub-menus the standard WordPress "sub-menu" class.
            $output .= "n$indent<ul class="sub-menu $current_sub_class">n";
    
            //We've now processed the sub-menu; reset the flag
            $this->is_current_submenu = FALSE;
        }
    
        function start_el( &$output, $item, $depth = 0, $args = array() ) {
            //Reset the flag such that "active" menu-items without sub-menus don't pass their "current" status on to the sub-menus of their siblings
            $this->is_current_submenu = FALSE;
    
            $indent = ( $depth ) ? str_repeat( "t", $depth ) : '';
    
            $class_names = $value = '';
            $classes = empty( $item->classes ) ? array() : (array) $item->classes;
            $classes[] = 'menu-item-' . $item->ID;
            $class_names = join( ' ', apply_filters( 'nav_menu_css_class', array_filter( $classes ), $item, $args ) );
            $class_names = $class_names ? ' class="' . esc_attr( $class_names ) . '"' : '';
    
                //Check to see if this element is "current" or a menu ancestor of "current" so that the "current" status may be passed on to any immediate sub-menu of the item.
                //You should add or remove conditions here in order to fit your application.
                //Also see "Final Notes" section below regarding performance
            if (strpos($class_names, 'current-menu-item')
                || strpos($class_names, 'current-menu-parent')
                || strpos($class_names, 'current-menu-ancestor')
                || strpos($class_names, 'current_page_parent'))
                $this->is_current_submenu = TRUE;
    
            $id = apply_filters( 'nav_menu_item_id', 'menu-item-'. $item->ID, $item, $args );
            $id = $id ? ' id="' . esc_attr( $id ) . '"' : '';
    
            $output .= $indent . '<li' . $id . $value . $class_names .'>';
    
            $attributes  = ! empty( $item->attr_title ) ? ' title="'  . esc_attr( $item->attr_title ) .'"' : '';
            $attributes .= ! empty( $item->target )     ? ' target="' . esc_attr( $item->target     ) .'"' : '';
            $attributes .= ! empty( $item->xfn )        ? ' rel="'    . esc_attr( $item->xfn        ) .'"' : '';
            $attributes .= ! empty( $item->url )        ? ' href="'   . esc_attr( $item->url        ) .'"' : '';
    
            $item_output = $args->before;
            $item_output .= '<a'. $attributes .'>';
            $item_output .= $args->link_before . apply_filters( 'the_title', $item->title, $item->ID ) . $args->link_after;
            $item_output .= '</a>';
            $item_output .= $args->after;
    
            $output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args );
        }
    }
    

    The entire contents of the referenced style.css file are as follows:

    ul.sub-menu {
        display:none;
    }
    ul.current-sub {
        display:block;
    }
    

    And finally, placing a call to wp_nav_menu() within a template file where the menu is to be displayed in order to make use of the new Walker (see the documentation for additional arguments):

    wp_nav_menu( array(
        'theme_location' => 'menu-97226',
        'walker' => new Menu_Walker_97226()
    ) );
    

    Final Notes

    • I have not run benchmarks, but it is entirely possible that may be quicker to check a menu-item’s classes using in_array() calls before the $classes array is joined rather than using strpos() calls afterwards. There’s also the possibility of using array_flip() and subsequently using isset( $classes[ $class_name ] ), though I suspect that the flip would defeat any performance gained from using isset().
    • Some part of my implementation was heavily inspired by a blog post that I’ve since lost track of in the months between. If I manage to locate it I will return to link it.
    • If any menu-items appear multiple times in your menu you may run into obscure behavior as all occurrences of a “current” menu item will maintain “current” status.
    • I’ve used the terms “current” and “active” somewhat interchangeably to refer to menu-items and sub-menus that should be displayed… My terminology is somewhat fuzzy and my head muddled at the moment – I’ll try to return later to clean it up 😉

    This may not be the exact solution you were looking for (and there may well exist better approaches to the issue), but I hope it’s enough to set you on the right path!

  2. I’m using the following code on one of my websites:

        if ( // Maybe you want different conditionals?
            (is_page() && ! is_front_page())
            || is_category()
        ) {
            $id = get_the_ID();
            $ancestors = get_ancestors($id, 'page');
            if (! empty($ancestors)) $id = $ancestors[count($ancestors)-1];
            $subpages = wp_list_pages('title_li=&child_of='.$id.'&depth=1&echo=0');
            if ('' !== $subpages) {
                echo '<nav id="subpages" role="navigation">'.PHP_EOL
                    .'<ul>'.PHP_EOL
                    .$subpages
                    .'</ul>'.PHP_EOL
                    .'</nav><!-- #subpages -->'.PHP_EOL;
            }
        }
    

    Please try this out and check for your desired features. If it doesn’t (exactly) do what you want, please let me know and I will try to update the code.