Creating a hierarchical submenu in WordPress

As I mentioned in Diving into WordPress I’ve started working with WordPress a bit. I’m getting the hang of it, but one thing that had me pulling my hair in frustration was creating a traditional hierarchical website submenu. I’m thinking of the kind of menu that normally appears in a sidebar when you’re in one of the sections of a horizontal top level menu.

There are multiple ways of creating menus in WordPress, but despite spending hours and hours searching the Web and testing plugins and code examples I couldn’t find a single example of a submenu that works the way I want it to (which happens to be the way submenus work in most other CMSs I have worked with).

So I ended up coding one myself.

Let’s say you have a website with several top level sections. Beneath one of those sections is the following page structure:

  • Page 1
    • Sub page 1-1
    • Sub page 1-2
      • Sub page 1-2-1
      • Sub page 1-2-2
      • Sub page 1-2-3
    • Sub page 1-3
      • Sub page 1-3-1
      • Sub page 1-3-2
  • Page 2
    • Sub page 2-1
    • Sub page 2-2
    • Sub page 2-3
  • Page 3
  • Page 4

What I want is that the submenu starts out completely collapsed when you’re on the top level page (the parent of this sub-tree):

  • Page 1
  • Page 2
  • Page 3
  • Page 4

When “Page 1” is the active page, its siblings and immediate children are displayed:

  • Page 1
    • Sub page 1-1
    • Sub page 1-2
    • Sub page 1-3
  • Page 2
  • Page 3
  • Page 4

Next, when “Sub page 1-2” is the active page, its parent (and the parent’s siblings), siblings and immediate children are displayed:

  • Page 1
    • Sub page 1-1
    • Sub page 1-2
      • Sub page 1-2-1
      • Sub page 1-2-2
      • Sub page 1-2-3
    • Sub page 1-3
  • Page 2
  • Page 3
  • Page 4

And so on, for however many levels deep the menu structure is.

I’ve found several ways of making WordPress render the entire page tree. That is not what I want. I also do not want the current page’s grandchildren. I want only the current sub-tree, and I want it to stop at the children of the current page.

Furthermore I do not want a solution that relies on CSS or JavaScript to hide the unwanted parts of the tree. For several reasons I want the HTML to reflect what is shown in the menu, nothing more.

Unless I’m missing something, both the wp_list_pages and wp_nav_menu functions appear incapable of doing this. It’s such a common way of rendering menus on CMS driven websites that I was a bit surprised when I couldn’t find any info on how to do it in WordPress.

Like I said, I ended up writing my own submenu function. It may contain bugs and conflict with PHP or WordPress best coding practices for all I know, but it outputs the HTML I want. So in case there are others going on the same WordPress menu hunt I did I thought I’d share what I came up with.

These two functions recursively create the menu:

function hierarchical_submenu($post) {
    $top_post = $post;
    // If the post has ancestors, get its ultimate parent and make that the top post
    if ($post->post_parent && $post->ancestors) {
        $top_post = get_post(end($post->ancestors));
    }
    // Always start traversing from the top of the tree
    return hierarchical_submenu_get_children($top_post, $post);
}

function hierarchical_submenu_get_children($post, $current_page) {
    $menu = '';
    // Get all immediate children of this page
    $children = get_pages('child_of=' . $post->ID . '&parent=' . $post->ID . '&sort_column=menu_order&sort_order=ASC');
    if ($children) {
        $menu = "\n<ul>\n";
        foreach ($children as $child) {
            // If the child is the viewed page or one of its ancestors, highlight it
            if (in_array($child->ID, get_post_ancestors($current_page)) || ($child->ID == $current_page->ID)) {
                $menu .= '<li class="sel"><a href="' . get_permalink($child) . '" class="sel"><strong>' . $child->post_title . '</strong></a>';
            } else {
                $menu .= '<li><a href="' . get_permalink($child) . '">' . $child->post_title . '</a>';
            }
            // If the page has children and is the viewed page or one of its ancestors, get its children
            if (get_children($child->ID) && (in_array($child->ID, get_post_ancestors($current_page)) || ($child->ID == $current_page->ID))) {
                $menu .= hierarchical_submenu_get_children($child, $current_page);
            }
            $menu .= "</li>\n";
        }
        $menu .= "</ul>\n";
    }
    return $menu;
}

That code goes in your functions.php file. The hierarchical_submenu_get_children function calls itself recursively until it has listed any children of the currently viewed page.

To insert a menu on a page, call it like this:

<?php echo hierarchical_submenu($post); ?>

$post is a reference to the page currently being viewed.

Alternatively, if you want to do something special on pages that do not have a submenu, call the function like this:

<?php
$submenu = hierarchical_submenu($post);
if ($submenu) {
    echo $submenu;
} else {
    // Do something else
}
?>

There has to be someone who has done this in WordPress before, but like I said I have not been able to find any descriptions of how to do it. Anyway, hopefully posting this will make it a bit easier for others who want to create a “normal” CMS-style submenu for a WordPress-driven site.

Update: For an alternative way of doing this, see Selective Page Hierarchy for wp_list_pages(), where Michael Fields explains how to extend the Walker_Page class. It’s a different approach that takes control of what wp_list_pages outputs rather than rewriting the functionality.

Posted on October 5, 2010 in WordPress, Content Management