Dynamically add items to WordPress menus

With the help of the wp_get_nav_menu_items filter, you can easily add dynamic links to a WordPress menu without having to concatenate html strings, or write a custom nav walker.

This example shows how to use the filter and construct a pseudo menu-item object (to meet the expectations of a nav walker class) to add a dynamic “My Profile” link to an existing menu.

The first thing we need is a simple helper function that creates a menu item object according to WordPress’ expectations.

/**
 * Simple helper function for make menu item objects
 * 
 * @param $title      - menu item title
 * @param $url        - menu item url
 * @param $order      - where the item should appear in the menu
 * @param int $parent - the item's parent item
 * @return \stdClass
 */ 
function _custom_nav_menu_item( $title, $url, $order, $parent = 0 ){
  $item = new stdClass();
  $item->ID = 1000000 + $order + parent;
  $item->db_id = $item->ID;
  $item->title = $title;
  $item->url = $url;
  $item->menu_order = $order;
  $item->menu_item_parent = $parent;
  $item->type = '';
  $item->object = '';
  $item->object_id = '';
  $item->classes = array();
  $item->target = '';
  $item->attr_title = '';
  $item->description = '';
  $item->xfn = '';
  $item->status = '';
  return $item;
}

To use this helper function, you need to take advantage of the wp_get_nav_menu_items filter so we can add menu items as desired:

add_filter( 'wp_get_nav_menu_items', 'custom_nav_menu_items', 20, 2 );

function custom_nav_menu_items( $items, $menu ){
  // only add item to a specific menu
  if ( $menu->slug == 'menu-1' ){
    
    // only add profile link if user is logged in
    if ( get_current_user_id() ){
      $items[] = _custom_nav_menu_item( 'My Profile', get_author_posts_url( get_current_user_id() ), 3 ); 
    }
  }
    
  return $items;
}

That’s it! To create a hierarchy (dropdown) of menu items, you could write your hook like this:

add_filter( 'wp_get_nav_menu_items', 'custom_nav_menu_items2', 20, 2 );

function custom_nav_menu_items2( $items, $menu ) {
  if ( $menu->slug == 'menu-1' ) {
    $top = _custom_nav_menu_item( 'Top level', '/some-url', 100 );

    $items[] = $top;
    $items[] = _custom_nav_menu_item( 'First Child', '/some-url', 101, $top->ID );
    $items[] = _custom_nav_menu_item( 'Third Child', '/some-url', 103, $top->ID );
    $items[] = _custom_nav_menu_item( 'Second Child', '/some-url', 102, $top->ID );
  }

  return $items;
}

Be careful. It’s important that no menu items share the same menu_order property.

Finally, I’ve created a class and example of its usage as a GitHub Repo: WordPress class to add custom menu items dynamically.  See the first comment on the gist for usage examples.

<?php

/**
 * Class custom_menu_items
 *
 * This class is for creating and managing dynamic menu items in a WordPress
 * menu.
 */
class custom_menu_items {
	/**
	 * Track that hooks have been registered w/ WP
	 * @var bool
	 */
	protected $has_registered = false;

	/**
	 * Internal list of menus affected
	 * @var array
	 */
	public $menus = array();

	/**
	 * Internal list of new menu items
	 * @var array
	 */
	public $menu_items = array();

	private function __construct(){}
	private function __wakeup() {}
	private function __clone() {}

	/**
	 * Singleton
	 *
	 * @return \custom_menu_items
	 */
	static public function get_instance(){
		static $instance = null;

		if ( is_null( $instance ) ){
			$instance = new self;
		}

		$instance->register();
		return $instance;
	}

	/**
	 * Hook up plugin with WP
	 */
	private function register(){
		if ( ! is_admin() && ! $this->has_registered ){
			$this->has_registered = true;

			add_filter( 'wp_get_nav_menu_items', array( $this, 'wp_get_nav_menu_items' ), 20, 2 );
			add_filter( 'wp_get_nav_menu_object', array( $this, 'wp_get_nav_menu_object' ), 20, 2 );
		}
	}

	/**
	 * Update the menu items count when building the menu
	 *
	 * @param $menu_obj
	 * @param $menu
	 *
	 * @return mixed
	 */
	function wp_get_nav_menu_object( $menu_obj, $menu ){
		if ( is_a( $menu_obj, 'WP_Term' ) && isset( $this->menus[ $menu_obj->slug ] ) ){
			$menu_obj->count += $this->count_menu_items( $menu_obj->slug );
		}
		return $menu_obj;
	}

	/**
	 * Get the menu items from WP and add our new ones
	 *
	 * @param $items
	 * @param $menu
	 *
	 * @return mixed
	 */
	function wp_get_nav_menu_items( $items, $menu ){
		if ( isset( $this->menus[ $menu->slug ] ) ) {
			$new_items = $this->get_menu_items( $menu->slug );

			if ( ! empty( $new_items ) ) {
				foreach ( $new_items as $new_item ) {
					$items[] = $this->make_item_obj( $new_item );
				}
			}

			$items = $this->fix_menu_orders( $items );
		}

		return $items;
	}

	/**
	 * Entry point.
	 * Add a new menu item to the list of custom menu items
	 *
	 * @param $menu_slug
	 * @param $title
	 * @param $url
	 * @param $order
	 * @param $parent
	 * @param null $ID
	 */
	static public function add_item( $menu_slug, $title, $url, $order = 0, $parent = 0, $ID = null, $classes = array() ){
		$instance = custom_menu_items::get_instance();
		$instance->menus[ $menu_slug ] = $menu_slug;
		$instance->menu_items[] = array(
			'menu'    => $menu_slug,
			'title'   => $title,
			'url'     => $url,
			'order'   => $order,
			'parent'  => $parent,
			'ID'      => $ID,
			'classes' => $classes,
		);
	}

	/**
	 * Add a WP_Post or WP_Term to the menu using the object ID.
	 *
	 * @param $menu_slug
	 * @param $object_ID
	 * @param string $object_type
	 * @param $order
	 * @param $parent
	 * @param null $ID
	 */
	static public function add_object( $menu_slug, $object_ID, $object_type = 'post', $order = 0, $parent = 0, $ID = NULL, $classes = array() ) {
		$instance = custom_menu_items::get_instance();
		$instance->menus[ $menu_slug ] = $menu_slug;

		if ($object_type == 'post' && $object = get_post( $object_ID ) ) {
			$instance->menu_items[] = array(
				'menu'        => $menu_slug,
				'order'       => $order,
				'parent'      => $parent,
				'post_parent' => $object->post_parent,
				'title'       => get_the_title($object),
				'url'         => get_permalink($object),
				'ID'          => $ID,
				'type'        => 'post_type',
				'object'      => get_post_type($object),
				'object_id'   => $object_ID,
				'classes'     => $classes,
			);
		}
		else if ($object_type == 'term') {
			global $wpdb;
			$sql = "SELECT t.*, tt.taxonomy, tt.parent FROM {$wpdb->terms} as t LEFT JOIN {$wpdb->term_taxonomy} as tt on tt.term_id = t.term_id WHERE t.term_id = %d";
			$object = $wpdb->get_row($wpdb->prepare($sql, $object_ID));

			if ( $object ) {
				$instance->menu_items[] = $tmp = array(
					'menu'        => $menu_slug,
					'order'       => $order,
					'parent'      => $parent,
					'post_parent' => $object->parent,
					'title'       => $object->name,
					'url'         => get_term_link((int)$object->term_id, $object->taxonomy),
					'ID'          => $ID,
					'type'        => 'taxonomy',
					'object'      => $object->taxonomy,
					'object_id'   => $object_ID,
					'classes'     => $classes,
				);
			}
		}
	}

	/**
	 * Get an array of new menu items for a specific menu slug
	 *
	 * @param $menu_slug
	 *
	 * @return array
	 */
	private function get_menu_items( $menu_slug ){
		$items = array();

		if ( isset( $this->menus[ $menu_slug ] ) ) {
			$items = array_filter( $this->menu_items, function ( $item ) use ( $menu_slug ) {
				return $item['menu'] == $menu_slug;
			} );
		}
		return $items;
	}

	/**
	 * Count the number of new menu items we are adding to an individual menu
	 *
	 * @param $menu_slug
	 *
	 * @return int
	 */
	private function count_menu_items( $menu_slug ){
		if ( ! isset( $this->menus[ $menu_slug ] ) ) {
			return 0;
		}

		$items = $this->get_menu_items( $menu_slug );

		return count( $items );
	}

	/**
	 * Helper to create item IDs
	 *
	 * @param $item
	 *
	 * @return int
	 */
	private function make_item_ID( $item ){
		return 1000000 + $item['order'] + $item['parent'];
	}

	/**
	 * Make a stored item array into a menu item object
	 *
	 * @param array $item
	 *
	 * @return mixed
	 */
	private function make_item_obj( $item ) {
		// generic object made to look like a post object
		$item_obj                   = new stdClass();
		$item_obj->ID               = ( $item['ID'] ) ? $item['ID'] : $this->make_item_ID( $item );
		$item_obj->title            = $item['title'];
		$item_obj->url              = $item['url'];
		$item_obj->menu_order       = $item['order'];
		$item_obj->menu_item_parent = $item['parent'];
		$item_obj->post_parent      = !empty( $item['post_parent'] ) ? $item['post_parent'] : '';

		// menu specific properties
		$item_obj->db_id            = $item_obj->ID;
		$item_obj->type             = !empty( $item['type'] ) ? $item['type'] : '';
		$item_obj->object           = !empty( $item['object'] ) ? $item['object'] : '';
		$item_obj->object_id        = !empty( $item['object_id'] ) ? $item['object_id'] : '';

		// output attributes
		$item_obj->classes          = $item['classes'];
		$item_obj->target           = '';
		$item_obj->attr_title       = '';
		$item_obj->description      = '';
		$item_obj->xfn              = '';
		$item_obj->status           = '';

		return $item_obj;
	}

	/**
	 * Menu items with the same menu_order property cause a conflict. This
	 * method attempts to provide each menu item with its own unique order value.
	 * Thanks @codepuncher
	 *
	 * @param $items
	 *
	 * @return mixed
	 */
	private function fix_menu_orders( $items ){
		$items = wp_list_sort( $items, 'menu_order' );

		for( $i = 0; $i < count( $items ); $i++ ){
			$items[ $i ]->menu_order = $i;
		}

		return $items;
	}
}
18 Thoughts

Discussion

Janos Ver
November 21, 2015

Thanks for this article. I have found it very useful. The only thing I’d like to add is that I discovered it today that once I added the above code to my site to replace a menu item based on user action broke the Appearance-> Customize menu and I got an error (Exception) saying some properties are missing. By looking at the object definition at https://developer.wordpress.org/reference/functions/wp_setup_nav_menu_item/ and the error message itself I figured out that the following were missing from _custom_nav_menu_item function (header and return just added to provide some context):

function _custom_nav_menu_item( $title, $url, $order, $parent = 0 ){

$item->target = ”;
$item->attr_title = ”;
$item->description = ”;
$item->xfn = ”;
$item->status = ”;


return $item;
}

I hope it saves some time and headache for others looking to use this filter.

Jonathan Daggerhart
November 21, 2015

Thanks Janos, I’ve updated the post.

Dave Spencer
December 17, 2015

I too found this useful. Care to expand it to so as to be able to create a menu with submenus? I would like to be able to inject an item in to the menu bar that has a dropdown with submenus below it. Just a thought – it would make this the ultimate guide!

Jonathan Daggerhart
December 30, 2015

Hi Dave,

I’ve updated the post to show how to create submenu items. Note: I had to make a small update to the “_custom_nav_menu_item” function (db_id) for this to work.

Let me know how it goes!

alan
December 23, 2015

Thanks for this snippet, just an small issue in your helper

$item->menu_parent_item = $parent;

should be

$item->menu_item_parent = $parent;

Jonathan Daggerhart
December 30, 2015

Thanks for the heads up! I’ve corrected the error in the post.

Cédric
June 29, 2016

Thank you very much for your article.
I create some pages dynamically using a PhP file. Adding your code at the beginning of the file, the page is correctly inserted in the menu but it’s not highlighted in menu and breadcrumb is not correct?
Any idea ?

Litbea
August 31, 2016

Jonathan, thanks a lot for this information.

I’m using your article to try and SUCCESSFULLY create sub-menu items with this changes and specifying an already created parent item:

function _custom_nav_menu_item( $title, $url, $order, $parent = 0 ){
$item = new stdClass();
$item->ID = 1000000 + $order + parent;
$item->db_id = $item->ID;
$item->title = $title;
$item->url = $url;
$item->menu_order = $order;
$item->menu_item_parent = PARENT_MENU_ITEM_ID;//$parent;
$item->type = ”;
$item->object = ”;
$item->object_id = ”;
$item->classes = array();
$item->target = ”;
$item->attr_title = ”;
$item->description = ”;
$item->xfn = ”;
$item->status = ”;
return $item;
}

add_filter( ‘wp_get_nav_menu_items’, ‘custom_nav_menu_items2’, 20, 2 );

function custom_nav_menu_items2( $items, $menu ) {
if ( $menu->slug == ‘menu-1’ ) {
//$top = _custom_nav_menu_item( ‘Top level’, ‘/some-url’, 100 );

//$items[] = $top;
//$items[] = _custom_nav_menu_item( ‘First Child’, ‘/some-url’, 101, $top->ID );
//$items[] = _custom_nav_menu_item( ‘Third Child’, ‘/some-url’, 103, $top->ID );
//$items[] = _custom_nav_menu_item( ‘Second Child’, ‘/some-url’, 102, $top->ID );
// the query
$wpb_all_query = new WP_Query(array(‘post_type’=>’custom_post_type_name’, ‘post_status’=>’publish’, ‘posts_per_page’=>-1, ‘orderby’ => ‘title’, ‘order’ => ‘ASC’));
if ( $wpb_all_query->have_posts() ) :
$subposition = 201;
while ( $wpb_all_query->have_posts() ) : $wpb_all_query->the_post();
// get the custom post data
$title = get_the_title();
$slug = basename(get_permalink());
// create the menu sub item
$items[] = _custom_nav_menu_item( $title, ‘/’.$slug, $subposition++, $top->ID );
endwhile;
wp_reset_postdata();
else :
echo ”. _e( ‘Sorry, no posts matched your criteria.’ ).”;
endif;
}

return $items;
}

Working great!

Litbea
August 31, 2016

Hi again, the only problem I’ve found is that a “The given object ID is not that of a menu item.” error shows up when saving the menu from /wp-admin/nav-menus.php

Any suggestion?

Lorenzo
August 23, 2017

Hello,
I’m trying to use your class in a plugin i’m working on but i got some errors.
here’s the relevant code samples:
inside my main plugin file:

add_action('wp_loaded', function () {
...
//that's the method that runs our code inside:
MenuWP::createWPMenuItems();
});

here’s the static function definition:

public static function creaWPMenuItems()
    {
        $menuTree = ['node'=>['parentMenu'=>'menu-1',
                              'text'=>'text menu item 1',
                              'url'=>'http://www.google.com',
                              'id'=>'ext_link']];
      foreach ($menuTree as $menuNode) {
          custom_menu_items::add_item($menuNode['parentMenu'], $menuNode['text'], $menuNode['url'], $menuNode['id']);
      }
    }

the item gets created and correctly displayed but then, whene i try to edit the menu from wp-admin/nav-menus.php i got the following errors and edits do not get saved:

E_NOTICE  Undefined property: stdClass::$post_excerpt - /vagrant/wp_clean/wp-content/themes/Avada/includes/class-avada-nav-walker-megamenu.php:166 
#5 /vagrant/wp_clean/wp-includes/class-wp-walker.php:146 - Avada_Nav_Walker_Megamenu->start_el('			display_element(stdClass, Array[0], 0, 0, Array[1], '			walk(Array[5], 0, stdClass)
#2 /vagrant/wp_clean/wp-admin/includes/nav-menu.php:958 - walk_nav_menu_tree(Array[5], 0, stdClass)
#1 /vagrant/wp_clean/wp-admin/nav-menus.php:496 - wp_get_nav_menu_to_edit(3)

Do you have any suggestions?

thanks!

Jonathan Daggerhart
August 23, 2017

Hi Lorenzo,

I haven’t run into this issue, but it seems you and plenty others have. In an attempt to fix this, I’ve updated the gist to now only execute on the frontend. I can’t really think of a reason why it should need to happen on the backend Dashboard, so hopefully this is a good fix.

If you get a chance to re-copy the gist and test it out, please let me know if it fixes the problem.

Thanks!

Lorenzo
August 23, 2017

Thanks! I’ll update and let you know asap :)

Lorenzo
August 23, 2017

Thanks!
The fix seems to work well, also the fact of not seeing the items in the dashboard is much more tidy.
Thanks a lot!

eduard
February 24, 2018

Thanks a lot!!!!
I was deseperate trying to do it, and thanks to your post, it was really easy!!

Saumil
June 22, 2018

Thank you so much for this article. It saved lot of my time and efforts finding and implementing this.

Hugh
September 1, 2018

SUPERB!!! Best code i could find for this !!! ;)

Luyen Dao
February 12, 2019

Hi, thank you for this – is it possible to add children to the sub-menu?

Parent Menu
— Child Level 1
—- Child Level 2
—- Child Level 2

I have a use case where I need to list posts (Child Level 2) by a taxonomy term (Child Level 1). My guess is that if I can reference Child Level 1 as a “parent ID”…i could do so, but not sure how. Any suggestions are greatly appreciated.

Digo
March 15, 2019

I tried the same, but it did not work.
Would love to have child level 2 menu :-)

any suggestions?

Kyle
October 7, 2019

If you\’re still trying to figure out how to do this, it\’s actually really simple! I needed three levels for a menu and this is the final parameter (@param null ID).

custom_menu_items::add_item( 'menu-1', 'Child 2', '/some-url/child-2', 0, 123456, 987654321 );
custom_menu_items::add_item( 'menu-1', 'Child 3', '/some-url/child-3', 0, 987654321 );

By passing in ID 987654321 to ‘Child 2’, I was able to use that as the $parent param for ‘Child 3’ which appears as a child of Child 3.

Julie
January 2, 2020

Kyle, thank you for providing how you were able to make the 3 level drop down menu. I followed your instructions and unfortunately I\’m not having any luck with this. Jonathan this is AWESOME! Thanks for all of your work! You helped me tremendously! If I can get the 3-level menu I\’ll be even happier. :-)

Julie
January 2, 2020

Alright I figured it out. I am using Bootstrap Navwalker for the menu and eventually realized that was the problem. I had to do the following to make it work:

1. Pass in the class of \’has-children\’ as an attribute:

custom_menu_items::add_item( \’menu-1\’, \’Child 2\’, \’/some-url/child-2\’, 0, 123456, 987654321, \’has-children\’ );

2. Modify \”depth\” value in Navwalker when you call wp_nav_menu.

\’depth\’ => 3,

3. Add the javascript on page load:

//for menu
jQuery(\’.dropdown-menu > li > .dropdown-menu\’).parent().addClass(\’dropdown-submenu\’).find(\’ > .dropdown-item\’).attr(\’href\’, \’javascript:;\’).addClass(\’dropdown-toggle\’);
jQuery(\’.dropdown-submenu > a\’).on(\”click\”, function(e) {
var dropdown = jQuery(this).parent().find(\’ > .show\’);
jQuery(\’.dropdown-submenu .dropdown-menu\’).not(dropdown).removeClass(\’show\’);
jQuery(this).next(\’.dropdown-menu\’).toggleClass(\’show\’);
e.stopPropagation();
});
jQuery(\’.dropdown\’).on(\”hidden.bs.dropdown\”, function() {
jQuery(\’.dropdown-menu.show\’).removeClass(\’show\’);
});

4. Add this to your style.css. After you\’re sure it\’s working, modify it to meet the look you want for your theme.

.dropdown-submenu {
position: relative;
}
.dropdown-submenu .dropdown-menu {
top: 0; left: 95%; margin-top: -1px;
}
@media (max-width: 992px)
{
.dropdown-menu {
padding: .5rem 0;
margin: .125rem 0 0;
}
li > ul > li > ul > li > .dropdown-item {
padding: .25rem 3rem;
}
li > ul > li > ul > li > .dropdown-item:before {
content: \’• \’;
}
}

Kai
March 7, 2019

Hi, thanks for this (in the meantime) “old” posting, it helped a lot.

But with php 7.1 you have to do some “Dr. Jekyll to Mr. Hyde Typecasting” in the helper function like this to avoid warnings.

In line 12 of the avove given code change

$item->ID = 1000000 + $order + parent;
to
$item->ID = 1000000 + (int)$order + (int)’parent’;

Carlos Alberto
July 10, 2019

Thanks Bro!!!! Found your article was so nice. Keep on the line

Barush Lakshman
August 29, 2019

Excellent informtion, thanks! Quick question: In the helper function you describe the 4th parameter as:
* @param int $parent – the item\\\’s parent item
So… how do you determine what value to pass as the \\\”parent item\\\”? (Sorry if that\\\’s a dumb question!)

Ömür Yanıkoğlu
April 27, 2020

This is just great! Thank you :)

Randy Abidin
July 18, 2020

Thank you so much for this. I’ve been wanting to programmatically create custom menus for a couple years now!!!
I’ve created a WP plugin based on your work at https://github.com/rabidin/dynamic-menus.
It allows the admin to control the display of the menus based on start/end times, is/is not logged in, is/is not front & Home page, URL parameters.

One of the issues I am having is I cannot get it to work in the footer. I am somewhat of an intermediate PHP programmer and i just cannot figure it out. I loaded your original GitHub code to verify it is not an issue with my code and it did not work in footer.

Any help/insight would be really appreciated!

Muhammad Zaheer Dad
October 7, 2020

To work with custom post types change line # 138
From : if ($object_type == \\\’post\\\’ && $object = get_post( $object_ID ) ) {

To: if (post_type_exists($object_type) && $object = get_post( $object_ID ) ) {

Lorant
August 3, 2021

excellent, thank you!

Leave a Reply

Your email address will not be published. Required fields are marked *