Add Custom Fields to WordPress Menu Items

A minimal working example of how to use the `wp_nav_menu_item_custom_fields` hook

First a little backstory

A WordPress commit that I am personally very excited to finally see get merged into core is the addition of a wp_nav_menu_item_custom_fields hook!

As the author of the Nav Menu Roles plugin, I’ve needed to display custom fields on menu items. But in the admin, the menu items are displayed by the Walker_Nav_Menu_Edit class and there have never been hooks in there to customize the output. The only 2 options for customizing the output were inserting fields via JavaScript or entirely replacing the Walker with your own customized Walker via the wp_edit_nav_menu_walker filter.

A WordPress Menu item metabox displaying options for hiding or showing this menu item to users based on their role.

The latter is what I did in my plugin, but the problem with the walker filter is that There Can Be Only One!

There Can Be Only One Highlander GIF - Find & Share on GIPHY

So other plugins (and themes…. cough…. soooo many themes!) that also needed to add custom fields would add their own Walkers and we’d duel it out to see whose fields got displayed.

Then WordPress user Shazdeh had the brilliant idea to start inserting a standard hook in our own Walkers… figuring that if we all added that hook to our Walker and then all attached our fields to that hook, it would not matter whose Walker was being used by core all our plugins could be compatible. We essentially established a “community” action hook and it greatly reduced the number of people contacting me wondering why Nav Menu Role’s fields were not displaying.

I expected that to be the permanent solution as the original Trac ticket was opened by @ViperBond back in 2011.

Originally it was blocked by another ticket concerning the limit of _POST variables… folks with ginormous menus would see their menus disappear when the _POST limit was exceeded and core didn’t want to make it easier for us to add fields. But that was fixed and this hook still languished. It would see a little traction and then nothing.

I actually went to my first contributor day to work on this specifically (it’s a 4 line patch, I thought it would be easy enough). I ended up spending the entire day trying to get VVV setup on my Windows machine so I could have the exact same environment as everyone else. Spoiler: I never did. I forget his name, but there was a gent who was trying so hard to help me and I thanked him, but it was so discouraging to go to a Contributor day and do nothing.

I’d pretty much given up hope on this one, but @MikeSchinkel picked it up and kept advocating for it. And now finally @SergeyBiryukov committed it! Even adding parity with the Customizer (which was another reason the patch had been rejected before). Hurray!!

Big thank you to everyone who has had a part in this over the years!

Leonardo Dicaprio Rap GIF - Find & Share on GIPHY

The Code Parts

Now that wp_nav_menu_item_custom_fields is in WordPress core, here’s a tutorial on how to use that new hook.

First, we’ll attach a callback to the new hook and use it to display a text input. Because the Walker displays multiple menu items we have to make sure the input is keyed with the menu item’s ID.

/**
* Add custom fields to menu item
*
* This will allow us to play nicely with any other plugin that is adding the same hook
*
* @param  int $item_id 
* @params obj $item - the menu item
* @params array $args
*/
function kia_custom_fields( $item_id, $item ) {

	wp_nonce_field( 'custom_menu_meta_nonce', '_custom_menu_meta_nonce_name' );
	$custom_menu_meta = get_post_meta( $item_id, '_custom_menu_meta', true );
	?>
	<div class="field-custom_menu_meta description-wide" style="margin: 5px 0;">
	    <span class="description"><?php _e( "Extra Field", 'custom-menu-meta' ); ?></span>
	    <br />

	    <input type="hidden" class="nav-menu-id" value="<?php echo $item_id ;?>" />

	    <div class="logged-input-holder">
	        <input type="text" name="custom_menu_meta[<?php echo $item_id ;?>]" id="custom-menu-meta-for-<?php echo $item_id ;?>" value="<?php echo esc_attr( $custom_menu_meta ); ?>" />
	        <label for="custom-menu-meta-for-<?php echo $item_id ;?>">
	            <?php _e( 'Custom menu text', 'custom-menu-meta'); ?>
	        </label>
	    </div>

	</div>

	<?php
}
add_action( 'wp_nav_menu_item_custom_fields', 'kia_custom_fields', 10, 2 );

Since menu items are a WordPress custom post type, dealing with the data is same as adding and retrieving post_meta a regular post or page.

/**
* Save the menu item meta
* 
* @param int $menu_id
* @param int $menu_item_db_id	
*/
function kia_nav_update( $menu_id, $menu_item_db_id ) {

	// Verify this came from our screen and with proper authorization.
	if ( ! isset( $_POST['_custom_menu_meta_nonce_name'] ) || ! wp_verify_nonce( $_POST['_custom_menu_meta_nonce_name'], 'custom_menu_meta_nonce' ) ) {
		return $menu_id;
	}

	if ( isset( $_POST['custom_menu_meta'][$menu_item_db_id]  ) ) {
		$sanitized_data = sanitize_text_field( $_POST['custom_menu_meta'][$menu_item_db_id] );
		update_post_meta( $menu_item_db_id, '_custom_menu_meta', $sanitized_data );
	} else {
		delete_post_meta( $menu_item_db_id, '_custom_menu_meta' );
	}
}
add_action( 'wp_update_nav_menu_item', 'kia_nav_update', 10, 2 );

Finally, what do we do with this new data!? While this isn’t what my plugin does, it’s the simplest use-case I could think of… displaying some extra text after the menu title.

/**
* Displays text on the front-end.
*
* @param string   $title The menu item's title.
* @param WP_Post  $item  The current menu item.
* @return string      
*/
function kia_custom_menu_title( $title, $item ) {

	if( is_object( $item ) && isset( $item->ID ) ) {

		$custom_menu_meta = get_post_meta( $item->ID, '_custom_menu_meta', true );

		if ( ! empty( $custom_menu_meta ) ) {
			$title .= ' - ' . $custom_menu_meta;
		}
	}
	return $title;
}
add_filter( 'nav_menu_item_title', 'kia_custom_menu_title', 10, 2 );

The three snippets together make a pretty solid minimal example for how to use this new hook. Full code can be downloaded here. At some point I should come back and do something with the new hooks in the Customizer too as I guess I finally need to update Nav Menu Roles!

6 Comments

  1. Spab on May 26, 2020 at 9:35 am

    Thank you very much for this tutorial.
    It works fine.

    However, I stumbled across an issue when I add a select box.
    When I add a menu item to the menu, the select is not displayed as selectbox, but instead in plain text?
    Do you have a solution for this? Am I missing something?

    See here:
    http://spab-rice.com/select.png

    Another issue is that the values of custom fields are not included to import/export.

  2. Sergey Belyaev on June 24, 2020 at 1:16 pm

    You use wp_nonce_field() function so you don’t need to include any hidden field for a nonce.

    • kathy on June 24, 2020 at 1:49 pm

      Good catch

  3. Peter Shaw on July 7, 2020 at 5:53 am

    Thi is a great tutorial and kudos for the addition to core. I am adding code based on this to one of my repisutory plugins.

    What is the aprent theme you are using here btw

    • kathy on July 7, 2020 at 4:07 pm

      Glad you liked it Peter. The theme is currently the Beaver Builder Theme.

Leave a Comment