Something I often see while trolling boards (and engaging in flame wars) is the question: “How do I display a menu with a parent level and child level, but only if the current page actually has child pages?” or something similar, but better phrased. The gist is, all the basic WordPress template functions don’t really do this, at least not very well. You can use a few functions to get something close, but it often is a compromise with the end result not quite what the designer had envisioned. I say no more!

What it actually does

The purpose of this article is to give you a function you can pop into a theme just like a regular template tag (wp_list_pages) but with some changed functionality. What you’ll get is a top level menu, nothing fancy, basic use of wp_list_pages really, but this top level is always there. You also get child menus, as many as you need. The child menus only display if the page actually has children, and it only displays one level of children instead of all of them.

The beginning of a beast

I’m not gonna lie, this is gonna get thick and fast, in fact the code barely makes sense to me and I wrote it. Here’s the short: we display a top level menu(easy), we check if a page is a child(easy), we check if the page has children itself(weird hack), we count the page’s parents and display the menu accordingly(huh?).

The top level

We’ll start with the top level menu, this is just a call to the wp_list_pages function, nothing fancy here. Output is generated with an unordered list with a few class names for styling.

//Multi-level pages menu
function wptt_multilevel_menu() {
	global $post;
	// Top level menu is always displayed
	$top_level = wp_list_pages('title_li=&depth=1&sort_column=menu_order&echo=0');

	// Put it all together
	$menu = '<ul class="menu top_level">';
	$menu .= $top_level;
	$menu .= '</ul>';

	print $menu;
}

Displaying the children

The meat of this example is this next part, displaying the children is in thought a very simple process, in practice it takes a bit of pounding a square peg into a round hole. We are still using the wp_list_pages function, and most of this code has one purpose, to determine the right depth to pass to wp_list_pages.

Initially we’ll check if a page has any parent pages using $post->ancestors and count the ancestors. I count the ancestors to determine the depth used later. Then check if a page has any more children. We’ll then separate those pages with children and those without.

<?php
// Check if a page has any parent pages
	if ($post->ancestors) {

		// How many ancestors does this page have? Then subtract by 1 to adjust for top level
		$n = count($post->ancestors);
		$n = $n - 1;

		// Get the page's children, if it has any
		$pages = get_pages();
		$page_children = get_page_children($post->ID, $pages);

		// Checks if a page has children
		if (!empty($page_children)) {
			$children = wp_list_pages("title_li=&child_of=". $post->ancestors[$n] ."&echo=0&sort_column=menu_order&depth=" . ($n + 2));
		} else { // If the page doesn't have children
			$children = wp_list_pages("title_li=&child_of=". $post->ancestors[$n] ."&echo=0&sort_column=menu_order&depth=" . ($n + 1));
		}

	} else {
		$children = wp_list_pages("title_li=&child_of=". $post->ID ."&echo=0&sort_column=menu_order&depth=1");
	}
?>

All of the counting done above($n, depth, ancestors, blah blah) is all done in order to display only the first child level. This way you are not showing all the children of the top level page.

The whole function

In the final function I’ve added some output for the children and combined everything above.

Updated 4/20/09 – the function has changed slightly since the explanation above.

//Multi-level pages menu
function wptt_multilevel_menu() {
	global $post;
	// Top level menu is always displayed
	$top_level = wp_list_pages('title_li=&depth=1&sort_column=menu_order&echo=0');

	// Get post ancestors 
	$post_ancestors = get_post_ancestors($post);

	// Check if a page has any parent pages
	if ($post_ancestors) {

		//get the top page id
		$top_page = $post_ancestors ? end($post_ancestors) : $post->ID;

		// How many ancestors does this page have? Counts the array adds one.
		$n = count($post_ancestors) + 1;

		// Get the pages children, if it has any
		$pages = get_pages();
		$page_children = get_page_children($post->ID, $pages);

		// Checks if a page has children
		if (!empty($page_children)) {
			$children = wp_list_pages("title_li=&child_of=". $top_page ."&echo=0&sort_column=menu_order&depth=" . $n);
		} else { // If the page doesn't have children
			$children = wp_list_pages("title_li=&child_of=". $top_page ."&echo=0&sort_column=menu_order&depth=" . ($n - 1));
		}

	} else {
		$children = wp_list_pages("title_li=&child_of=". $post->ID ."&echo=0&sort_column=menu_order&depth=1");
	}

	// Put it all together
	$menu = '<ul class="menu top_level">';
	$menu .= $top_level;
	$menu .= '</ul>';
	// Only show child navigation if there are children
	if ( $children ) {
		$menu .= '<ul class="menu subpages">';
		$menu .= $children;
		$menu .= '</ul>';
	}
	print $menu;
}

The future and spaceships

That is one way to do multiple level menus in wordpress, right now this function is only using pages, but I will be expanding it to use categories as well. There are actually quite a few ways I want to expand this in order to make it truly useful. This function is part of the larger WordPress Theme Tools project that I’m starting. If you have any suggestions for this, find a bug, or know a better way, let me know!

Note: I’ve updated the code on 4/20/09 but the function still works the same. As it is right now the sub pages are displayed in the default nested way that WordPress naturally does it. The benefit of the function is that it only shows one level under the parent page instead of hard coding the depth or showing all the children.