Add Custom Fields to WordPress Navigation Menu Items with wp_nav_menu_item_custom_fields

WordPress navigation menus support custom fields per menu item — data stored as post meta on the underlying nav_menu_item post type. These fields let you attach an icon class, a “New!” badge label, a custom CSS class beyond what WordPress provides in the “CSS Classes” field, an ARIA description, or a mega-menu column count to each individual link in the menu. WordPress 5.4 added the wp_nav_menu_item_custom_fields action hook, which fires inside the menu item edit form in the admin, giving plugins and themes a supported way to add input fields without hacking the Walker. Saving uses the wp_update_nav_menu_item hook which fires after each menu item is saved. Reading the stored value in a template uses get_post_meta() on the menu item’s post ID. For outputting the value in the rendered HTML, you can use a nav_menu_link_attributes filter for simple attribute injection, or a custom Walker for more complex markup changes.

Problem: You want editors to assign a Font Awesome icon class to each navigation menu item from the menu editor, so the rendered menu outputs <i class="fa fa-home"></i> before each link label.

Solution: Add an icon field to the menu item form via wp_nav_menu_item_custom_fields, save it with update_post_meta() on wp_update_nav_menu_item, and inject the icon HTML via the walker_nav_menu_start_el filter.

<?php
// ── 1. Add field to the menu item editor ──────────────────────────────
add_action( 'wp_nav_menu_item_custom_fields', 'add_menu_item_icon_field', 10, 4 );

function add_menu_item_icon_field( $item_id, $item, $depth, $args ) {
    $icon = esc_attr( 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 Class (e.g. fa fa-home)', 'textdomain' ); ?><br>
            <input type="text"
                   id="edit-menu-item-icon-<?php echo esc_attr( $item_id ); ?>"
                   class="widefat"
                   name="menu-item-icon[<?php echo esc_attr( $item_id ); ?>]"
                   value="<?php echo $icon; ?>">
        </label>
    </p>
    <?php
}

// ── 2. Save the field when the menu is saved ───────────────────────────
add_action( 'wp_update_nav_menu_item', 'save_menu_item_icon_field', 10, 3 );

function save_menu_item_icon_field( $menu_id, $menu_item_db_id, $menu_item_data ) {
    if ( isset( $_POST['menu-item-icon'][ $menu_item_db_id ] ) ) {
        $icon = sanitize_html_class( $_POST['menu-item-icon'][ $menu_item_db_id ] );
        update_post_meta( $menu_item_db_id, '_menu_item_icon', $icon );
    } else {
        delete_post_meta( $menu_item_db_id, '_menu_item_icon' );
    }
}

// ── 3. Inject the icon into the rendered menu link ────────────────────
add_filter( 'walker_nav_menu_start_el', 'prepend_icon_to_menu_item', 10, 4 );

function prepend_icon_to_menu_item( $item_output, $item, $depth, $args ) {
    $icon = get_post_meta( $item->ID, '_menu_item_icon', true );

    if ( $icon ) {
        $icon_html   = '<i class="' . esc_attr( $icon ) . '" aria-hidden="true"></i>';
        $item_output = str_replace( $args->link_before, $args->link_before . $icon_html, $item_output );
    }

    return $item_output;
}

NOTE: The wp_nav_menu_item_custom_fields action was added in WordPress 5.4. For older WordPress versions the same effect requires overriding the Walker_Nav_Menu_Edit class — significantly more code. If you need to support WP < 5.4, use the wp_edit_nav_menu_walker filter to substitute a custom walker class that extends Walker_Nav_Menu_Edit and overrides the start_el method.