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.
42 Comments
Leave a Commentyou can hide children pages you don’t want to display with css as well.
24th Feb 2009
yeah but you would still need to somehow set dynamic classes for each page through php in order to hide and display the right menus
24th Feb 2009
Hello, iam trying do my own plugins to the wordpress please help me
6th Mar 2009
I’ve just tried it out, but it fails to display the third level of pages..
I get a structure like this:
A—B—C—D
B1–B2
B2a-B2b-B2c–B3–B4
…where everything from the B2-level is displayed as nested uls.
8th Mar 2009
I didn’t even realize this while testing, I’m probably going to have to rethink the output code.
Anyway, there is an easy way to fix this and that is to absolutely position .subpages ul relative to .subpages.
I’m not sure if it is going to be better to leave the uls nested or separate them for easier use.
8th Mar 2009
If working with a really dynamic menu, absolute positioning is not really an option…
9th Mar 2009
For us dummies where do we put the code. The theme I’m using now don’t have or want display child pages at all. Thanks, Richard
8th Mar 2009
Copy the whole function into your theme’s functions.php file(or create one) then in your templates add
<?php wptt_multilevel_menu(); ?>. It works just like a template tag.But as Michael pointed out you are going to have to do some CSS to make it behave right.
8th Mar 2009
what kind of CSS do I need to do to make it behave right? I can not even get the first level child pages to show up.
14th May 2010
The CSS is to absolutely position the children under the parent.
If you can’t see the children in the HTML then something else is the problem.
14th May 2010
Curtis, thanks a million for this post! I’ve been searching for so long trying to find this functionality, but never found anything that got it quite this right, whether it was a core function or a plugin.
I’ve not built up the whole structure yet as I’m in the middle of a project, but I’m outputting this as a submenu, starting at level 2 and it shows correctly for me down to level 4, which is where I stop.
Fingers crossed it wont throw any spanners in the works when adding more subpages in the wider structure – if not, this is the most helpful WP-related post I’ve read for a very long time, as it solves a problem I’ve had ever since I started using WP for slightly larger sites.
Thanks!
19th Mar 2009
Thanks for the kind words, I’m so happy when I know something like this actually helped someone.
As long as you can use absolute positioning like I described above this should work. I am going to re-write it so that won’t be necessary in the future.
19th Mar 2009
We just released a plugin that offers this type of functionality, with even more control over the HTML output, very easily. We’d love your feedback:
http://www.cmurrayconsulting.com/software/wordpress-simple-section-navigation/
1st Apr 2009
Jake,
Great job, I added a link in the above post. I haven’t had time to improve on this since the initial idea so I’m glad someone else took the time to do it right.
1st Apr 2009
i needed a static “top level” and “2nd level”.. simply that
so i removed the
// Checks if a page has children statement to make it always without children and changed the depth parameter “=menu_order&depth=” . ($n – 1)); ” to “=menu_order&depth=1); ” so i always get only the 2nd level of my ancestor page.
hope this helps some1
thank you so much for the code
2nd May 2009
Oh my goodness I spent 5 hours trying to finagle some CSS because all the plugins I could find kept putting the children into a new ul inside of the parent li. This is exactly what I needed…. I need to go celebrate.
6th May 2009
Great, thanks! ^_^
4th Jun 2009
If I use:
it shows only the LIST,
While I want TO OUTPUT the CONTENT of the PAGE, not the list
10th Jun 2009
To output the content it is the_content() function inside the loop
10th Jun 2009
wp_list_pages(‘echo=1′);
10th Jun 2009
I gave it a try but no matter what I do, the children links always print at the bottom of the list of all parents.
-Parent1
-Parent2
-Parent3
it should be
-Parent1
–Child1 of Parent1
–Child2 of Parent2
-Parent2
-Parent3
Any help would be much appreciated. I checked out the “Simple Section Nav” as well, but that doesn’t print the Parents.
28th Oct 2009
I assume you have a vertical navigation, this had a horizontal nav in mind where I needed to separate the child elements into a new div.
You could probably use the default wp_list_pages and then use CSS to show and hide the child pages depending on the active page and parent. I believe WP gives you the css classes to do this.
28th Oct 2009
Correct – it is a vertical nav.
The problem is that I can’t actually get the children to print in a new UL inside of the parent LI.
I’m really surprised that a solution isn’t readily available. It seems like a pretty common setup
28th Oct 2009
thx 4 the plugin, but i’ve got also a problem:
I’ve got a Menu like that:
1
1-1
1-1-1
1-1-2
1-2
1-3
1-3-1
1-3-2
2
…
3
…
So the Problem is now, that if i click on 1-1-1 i got all child-pages from 1. That means, that all children of 1-1 are displayed (that correct) but also all children of 1-3 (that’s not correct)
Any idea how to fix this? Or a plugin which can do this for me? The one above works with sections and not with pages
greetz
2nd Dec 2009
Peter,
Been thinking about this but don’t have a solution yet.
The problem is the way the current script works is it looks for the top level page and then displays all the children below it. I think what you need is for the script to find the first child of the top page, and to display the children of that page. But this would probably break the menu at the top level.
7th Dec 2009
Thank you for an article, it helped me.
Just one question — is there any way to show children of *only* current subpage?
E.g. I have page’s structure like this:
Main-
-page1
–subpage1-1
–subpage1-2
-page2
-page3
–subpage3-1
–subpage3-2
So, I need to show subpages 3-1 and 3-2 *only* if page3 is current one, and not show subpages 1-1 and 1-2.
Any advise will be appriciated.
31st Jan 2010
The only way to build such a list, as far as I figured it out, is to build array with pages ID and then show only selected pages according *only* on selected IDs.
31st Jan 2010
It’s true that the function currently shows all child and grandchild pages of the active parent. I like the idea of building an array, but using page ID’s would not be a ‘dynamic’ solution to the problem. If I could write this myself, believe me, I would! So thanks for the function and the discussion.
28th Apr 2010
Sergei,
That’s what the function should be doing right now. Maybe I misunderstood what you needed.
But if you are on Page 3, 3-1 and 3-2 should show but 1-1 and 1-2 should not.
5th Feb 2010
You rock! I just spent almost 2 hours trying to find a solution like this and wordpress made this seem really hard… well done to you and thank you again!
28th Feb 2010
Hi,
i would really appreciate an advice :
my menu is:
item1
item2
item3
but i would like to get if i am on the page 2 for example a new order:
item2
item1
item3
How can i fix that?i am lost.
my code is:
post_parent)
$children = wp_list_pages(‘sort_column=menu_order&title_li=&child_of=’.$post->post_parent.’&echo=0′);
else
$children = wp_list_pages(‘sort_column=menu_order&title_li=&child_of=’.$post->ID.’&echo=0′);
if ($children) {
?>
prueba
i knor the problem is `sort_column=menu_order’but don t manage to find the right tag to solve my problem.
your help would be waouu!:)
10th Mar 2010
Make 2 calls. One to display page 2, and one to display the rest.
so the first one would be something like $titlenamer=get_the_title(post->ID);
echo $titlenamer;
and the second you would put an ‘exclude=’.$post->ID. into the wp_list_pages to remove it from the bottom list.
12th Mar 2010
Yeah this is going to be the most elegant solution David.
12th Mar 2010
Nice form you got here!
I’m in the same boat with Jared from above, looking for a vertical nav and trying to modify your function to place the children of the parent inside the ul of that parent.
Any ideas?
6th Apr 2010
Hey,
I like to keep things simple.
What I am using is this:
ID.'&echo=0');
if($children) {
$has_children = true;
}
// case: child page
if(is_page() && $post->post_parent) {
// This is a subpage
$menu = wp_list_pages('sort_column=menu_order&title_li=&depth=0&echo=0');
// case: parent page
} else if($has_children) {
// This is a parent page that have subpages
$menu = wp_list_pages('sort_column=menu_order&title_li=&depth=0&echo=0');
}
// case: not a parent page and not a child page
else {
$menu = wp_list_pages('sort_column=menu_order&title_li=&depth=1&echo=0');
}
?>
And the rest is just CSS.
Of course it is a different style of navigation but it works fine for most websites I am developing.
:)
21st Apr 2010
Are you using this in addition to the function, or on it’s own?
Also, does the first line:
ID.'&echo=0');have to do with the menu?
28th Apr 2010
Sebastian’s code was mangled I think that first line should read something like:
$children = wp_list_pages("title_li=&child_of=". $post->ID ."&echo=0");28th Apr 2010
Thanks, that makes sense. Still working on the array concept that would build a list of child and grandchild pages for the current parent, then only show those child pages of the current parent, and only show those grandchild pages of the current child. I’ll post back if I come up with anything.
28th Apr 2010
Awesome!
Just what I was looking for. You’re the best man!
Thanks so much, works perfectly.
27th Apr 2010
Thank you so much. However, I have a question.
Is it possible to move the childmenu to a different block(div) than where the mastermenu is?
14th May 2010
Not the way it is currently written, but could be possible. Maybe if you put the child menu into a variable and then called it somewhere else in the template or passed it into another function.
14th May 2010