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 Content Management, WordPress

Comments are disabled for this post (read why), but if you have spotted an error or have additional info that you think should be in this post, feel free to contact me.