In the recent article, Zack Pyle mentioned the ability to add images to the menu manager, and he has a snippet on his SnippetNest site that works well. One thing it doesn’t do is extend the icons to the Customizer interface, which is my preferred way to manage the menus. I asked ChatGPT if it was possible, and 90 seconds later I had a working solution.
Here is the code:
// Enqueue media library + picker script for nav menus
add_action( 'admin_enqueue_scripts', function( $hook ) {
if ( $hook !== 'nav-menus.php' ) return;
wp_enqueue_media();
wp_add_inline_script( 'jquery', '
jQuery(function($) {
$(document).on("click", ".menu-item-icon-select", function(e) {
e.preventDefault();
var button = $(this);
var container = button.closest(".field-icon");
var input = container.find(".edit-menu-item-icon");
var preview = container.find(".menu-item-icon-preview");
var frame = wp.media({
title: "Select Icon Image",
button: { text: "Use this image" },
multiple: false
});
frame.on("select", function() {
var attachment = frame.state().get("selection").first().toJSON();
input.val(attachment.url);
preview.attr("src", attachment.url).show();
container.find(".menu-item-icon-remove").show();
});
frame.open();
});
$(document).on("click", ".menu-item-icon-remove", function(e) {
e.preventDefault();
var container = $(this).closest(".field-icon");
container.find(".edit-menu-item-icon").val("");
container.find(".menu-item-icon-preview").hide();
$(this).hide();
});
});
' );
} );
// Add icon field to menu items
add_action( 'wp_nav_menu_item_custom_fields', function( $item_id, $item, $depth, $args ) {
$icon = get_post_meta( $item_id, '_menu_item_icon', true );
?>
<p class="field-icon description description-wide">
<label for="edit-menu-item-icon-<?php echo esc_attr( $item_id ); ?>">
<?php esc_html_e( 'Icon Image', 'your-textdomain' ); ?>
</label>
<br />
<input type="hidden"
id="edit-menu-item-icon-<?php echo esc_attr( $item_id ); ?>"
class="edit-menu-item-icon"
name="menu-item-icon[<?php echo esc_attr( $item_id ); ?>]"
value="<?php echo esc_attr( $icon ); ?>" />
<?php if ( $icon ) : ?>
<img src="<?php echo esc_url( $icon ); ?>"
class="menu-item-icon-preview"
style="max-width:60px;max-height:60px;display:block;margin:6px 0;" />
<?php else : ?>
<img class="menu-item-icon-preview"
style="max-width:60px;max-height:60px;display:none;margin:6px 0;" />
<?php endif; ?>
<button type="button" class="button menu-item-icon-select">
<?php esc_html_e( $icon ? 'Change Image' : 'Select Image', 'your-textdomain' ); ?>
</button>
<button type="button"
class="button-link menu-item-icon-remove"
style="color:#b32d2e;margin-left:8px;<?php echo $icon ? '' : 'display:none;'; ?>">
<?php esc_html_e( 'Remove', 'your-textdomain' ); ?>
</button>
</p>
<?php
}, 10, 4 );
// Save the icon field
add_action( 'wp_update_nav_menu_item', function( $menu_id, $menu_item_db_id, $args ) {
// Only process submissions from Appearance > Menus.
if (
! isset( $_POST['menu-item-icon'] ) ||
! is_array( $_POST['menu-item-icon'] )
) {
return;
}
$raw = $_POST['menu-item-icon'][ $menu_item_db_id ] ?? '';
$icon = esc_url_raw( wp_unslash( $raw ) );
if ( $icon ) {
update_post_meta( $menu_item_db_id, '_menu_item_icon', $icon );
} else {
delete_post_meta( $menu_item_db_id, '_menu_item_icon' );
}
}, 10, 3 );
/**
* Make the saved icon available to the Customizer's JavaScript setting.
*/
add_filter( 'wp_setup_nav_menu_item', function( $item ) {
if ( ! empty( $item->ID ) ) {
$item->menu_item_icon = get_post_meta(
$item->ID,
'_menu_item_icon',
true
);
}
return $item;
} );
/**
* Add the icon controls to each menu item in the Customizer.
*/
add_action( 'wp_nav_menu_item_custom_fields_customize_template', function() {
?>
<p class="field-icon description description-wide">
<span class="customize-control-title">
<?php esc_html_e( 'Icon Image', 'your-textdomain' ); ?>
</span>
<img
class="menu-item-icon-preview"
src=""
alt=""
style="max-width:60px;max-height:60px;display:none;margin:6px 0;"
/>
<button type="button" class="button menu-item-icon-select">
<?php esc_html_e( 'Select Image', 'your-textdomain' ); ?>
</button>
<button
type="button"
class="button-link menu-item-icon-remove"
style="color:#b32d2e;margin-left:8px;display:none;"
>
<?php esc_html_e( 'Remove', 'your-textdomain' ); ?>
</button>
</p>
<?php
} );
/**
* Load the media picker and connect the custom field to each
* Customizer menu-item setting.
*/
add_action( 'customize_controls_enqueue_scripts', function() {
wp_enqueue_media();
wp_enqueue_script( 'customize-nav-menus' );
wp_add_inline_script(
'customize-nav-menus',
<<<'JS'
(function ($, api) {
function updateSetting(control, iconUrl) {
var value = _.clone(control.setting());
value.menu_item_icon = iconUrl;
control.setting.set(value);
}
function updateFields(control) {
var container = control.container.find('.field-icon');
var preview = container.find('.menu-item-icon-preview');
var select = container.find('.menu-item-icon-select');
var remove = container.find('.menu-item-icon-remove');
var value = control.setting();
var iconUrl = value.menu_item_icon || '';
if (iconUrl) {
preview.attr('src', iconUrl).show();
select.text('Change Image');
remove.show();
} else {
preview.attr('src', '').hide();
select.text('Select Image');
remove.hide();
}
}
function extendMenuItemControl(control) {
var container = control.container.find('.field-icon');
if (!container.length || container.data('icon-control-ready')) {
return;
}
container.data('icon-control-ready', true);
updateFields(control);
control.setting.bind(function () {
updateFields(control);
});
container.on('click', '.menu-item-icon-select', function (event) {
var frame;
event.preventDefault();
frame = wp.media({
title: 'Select Icon Image',
button: {
text: 'Use this image'
},
multiple: false
});
frame.on('select', function () {
var attachment = frame
.state()
.get('selection')
.first()
.toJSON();
updateSetting(control, attachment.url);
});
frame.open();
});
container.on('click', '.menu-item-icon-remove', function (event) {
event.preventDefault();
updateSetting(control, '');
});
}
api.control.bind('add', function (control) {
if (!control.extended(api.Menus.MenuItemControl)) {
return;
}
control.deferred.embedded.done(function () {
extendMenuItemControl(control);
});
});
})(jQuery, wp.customize);
JS
);
} );
/**
* Save the icon metadata after the Customizer publishes its changes.
*/
add_action( 'customize_save_after', function( WP_Customize_Manager $wp_customize ) {
$posted_values = $wp_customize->unsanitized_post_values();
foreach ( $posted_values as $setting_id => $value ) {
if (
! preg_match(
'/^nav_menu_item\[-?\d+\]$/',
$setting_id
) ||
! is_array( $value ) ||
! array_key_exists( 'menu_item_icon', $value )
) {
continue;
}
$setting = $wp_customize->get_setting( $setting_id );
if (
! $setting instanceof WP_Customize_Nav_Menu_Item_Setting ||
! $setting->check_capabilities()
) {
continue;
}
// Placeholder IDs for new items are replaced during publishing.
$menu_item_id = (int) $setting->post_id;
if ( $menu_item_id <= 0 ) {
continue;
}
$icon = esc_url_raw( $value['menu_item_icon'] );
if ( $icon ) {
update_post_meta(
$menu_item_id,
'_menu_item_icon',
$icon
);
} else {
delete_post_meta(
$menu_item_id,
'_menu_item_icon'
);
}
}
}, 20 );As Zack mentions on his site, this is the code needed to add it to the menu loop. I opted to put them in the same snippet:
// Expose the icon field in Etch templates
add_filter( 'wp_menus_for_etch/item', function( $node, $item ) {
$icon = get_post_meta( $item->ID, '_menu_item_icon', true );
if ( ! empty( $icon ) ) {
$node['icon'] = $icon;
}
return $node;
}, 10, 2 );