WordPress provides a robust menu system, but sometimes you need to extend it with custom fields for menu items. Here’s how to do it properly:
1. Adding Custom Fields to Menu Items
Using the wp_nav_menu_item_custom_fields hook (WordPress 5.4+)
add_action('wp_nav_menu_item_custom_fields', 'add_custom_menu_fields', 10, 4);
function add_custom_menu_fields($item_id, $item, $depth, $args) {
?>
<div class="field-custom_field description description-wide">
<label for="edit-menu-item-custom-<?php echo $item_id; ?>">
<?php _e('Custom Field'); ?><br />
<input type="text" id="edit-menu-item-custom-<?php echo $item_id; ?>"
class="widefat code edit-menu-item-custom"
name="menu-item-custom[<?php echo $item_id; ?>]"
value="<?php echo esc_attr($item->custom); ?>" />
</label>
</div>
<?php
}
For older WordPress versions (pre-5.4)
add_filter('wp_edit_nav_menu_walker', 'custom_menu_walker', 10, 2);
function custom_menu_walker($walker, $menu_id) {
return 'Walker_Nav_Menu_Edit_Custom';
}
// Create a custom walker
require_once ABSPATH . 'wp-admin/includes/nav-menu.php';
class Walker_Nav_Menu_Edit_Custom extends Walker_Nav_Menu_Edit {
function start_el(&$output, $item, $depth = 0, $args = array(), $id = 0) {
parent::start_el($output, $item, $depth, $args, $id);
// Add your custom fields here
$custom_field = get_post_meta($item->ID, '_menu_item_custom', true);
$output = str_replace(
'<fieldset class="field-move hide-if-no-js">',
'<div class="field-custom description description-wide">
<label for="edit-menu-item-custom-' . $item->ID . '">
' . __('Custom Field') . '<br />
<input type="text" id="edit-menu-item-custom-' . $item->ID . '"
class="widefat code edit-menu-item-custom"
name="menu-item-custom[' . $item->ID . ']"
value="' . esc_attr($custom_field) . '" />
</label>
</div>
<fieldset class="field-move hide-if-no-js">',
$output
);
}
}
2. Saving Custom Fields
add_action('wp_update_nav_menu_item', 'save_custom_menu_fields', 10, 3);
function save_custom_menu_fields($menu_id, $menu_item_db_id, $args) {
if (isset($_REQUEST['menu-item-custom'][$menu_item_db_id])) {
$custom_value = sanitize_text_field($_REQUEST['menu-item-custom'][$menu_item_db_id]);
update_post_meta($menu_item_db_id, '_menu_item_custom', $custom_value);
}
}
3. Previewing Custom Fields in the Menu
To see your custom fields in the menu preview
add_filter('wp_setup_nav_menu_item', 'setup_custom_nav_fields');
function setup_custom_nav_fields($menu_item) {
$menu_item->custom = get_post_meta($menu_item->ID, '_menu_item_custom', true);
return $menu_item;
}
4. Displaying Custom Fields in Frontend
class Custom_Walker_Nav_Menu extends Walker_Nav_Menu {
function start_el(&$output, $item, $depth = 0, $args = array(), $id = 0) {
$attributes = '';
$description = '';
!empty($item->custom) && $attributes .= ' data-custom="' . esc_attr($item->custom) . '"';
$item_output = $args->before;
$item_output .= '<a' . $attributes . ' href="' . esc_attr($item->url) . '">';
$item_output .= $args->link_before . apply_filters('the_title', $item->title, $item->ID) . $args->link_after;
$item_output .= '</a>';
$item_output .= $args->after;
$output .= apply_filters('walker_nav_menu_start_el', $item_output, $item, $depth, $args);
}
}
Then use your custom walker when calling wp_nav_menu():
wp_nav_menu(array(
'theme_location' => 'primary',
'walker' => new Custom_Walker_Nav_Menu()
));
5. Adding JavaScript for Real-time Preview
jQuery(document).ready(function($) {
// Watch for changes in custom fields and update preview
$(document).on('change', '.edit-menu-item-custom', function() {
var input = $(this),
menuItem = input.closest('.menu-item'),
itemId = menuItem.find('.menu-item-data-db-id').val(),
preview = $('#menu-item-preview-' + itemId);
// Update preview element
preview.find('.menu-item-title').attr('data-custom', input.val());
});
});
Best Practices
- Always sanitize input when saving
- Use proper escaping when outputting values
- Prefix your custom field names to avoid conflicts
- Consider using WordPress’s built-in REST API for more complex previews
- Test with accessibility in mind
This approach gives you a solid foundation for extending WordPress menu items while maintaining compatibility with core functionality.