Allow users with Editor role to edit menus (without a plugin)

Recently I had to tackle the same issue. Instead of setting the capability permanently for editors, I used user_has_cap filter to allow access dynamically.

add_filter('user_has_cap', 'editor_allow_edit_menu', 10, 4);
function editor_allow_edit_menu($allcaps, $caps, $args, $user) {

  if ( ! isset( $allcaps['editor'] ) || true !== $allcaps['editor'] ) {
    return $allcaps;
  }

  global $pagenow;
  // new items are added to the menu with ajax
  // you could perhaps also check here that the ajax action is 'add-menu_item' from $_POST
  if ( wp_doing_ajax() || 'nav-menus.php' === $pagenow ) {
    $allcaps['edit_theme_options'] = true;
  }

  return $allcaps;
}

The above code allows the user to access the menu editing page and add new items to menu, which happens by admin-ajax action. This however doesn’t display a link to the editing page. For that I added a custom link to the admin menu.

add_action( 'admin_menu', 'add_nav_menus_link_for_editor' );
function add_nav_menus_link_for_editor() {
  if ( ! current_user_can('editor') ) {
    return;
  }
  // Custom link to the nav menus page
  add_menu_page(
    __('Menus'),
    __('Menus'),
    'edit_others_posts',
    '?options=nav-menus.php', // I didn't get 'nav-menus.php' working directly here and didn't bother figuring out why
    '',
    'dashicons-menu',
    90
  );
  // remove themes menu page, would be visible otherwise when user is on menu editing page and has the edit_theme_options capability
  remove_menu_page('themes.php');
}

As I didn’t get nav-menus.php working when used directly on add_menu_page() as the page slug, I added quick and dirty redirect switcheroo to direct the user to the menu editing page when one clicks the custom menu link.

add_action('admin_init', 'redirect_to_nav_menu_page');
function redirect_to_nav_menu_page() {
  if ( current_user_can('editor') && isset($_GET['options']) && 'nav-menus.php' === $_GET['options'] ) {
    wp_redirect( admin_url( 'nav-menus.php' ) );
    die;
  }
}

I think this way access is more limited as the capability is only set when using one specific admin page. Trying to directly access any other settings page will result in capabilities error notification.

This is simplified version of the code I used. Originally I had a fancy loop that allowed similar access for editors to few other settings pages, too, based on one config array.