Customizing the WordPress admin menu is something you’ll eventually need to or want to do at some point. Customizing admin menus with Post Types is pretty easy compared to Taxonomies.

When you create Custom Post Types and Custom Taxonomies in WordPress using register_post_type(); and register_taxonomy(); there’s a parameter for each function called show_in_menu.

If you look at register_post_type(); the parameter accepts a simple true or false but also accepts a string as an argument for show_in_menu:

show_in_menu
(boolean or string) (optional) Where to show the post type in the admin menu. show_ui must be true.

Default: value of show_ui argument

‘false’ – do not display in the admin menu
‘true’ – display as a top level menu
‘some string’ – If an existing top level page such as 'tools.php' or 'edit.php?post_type=page', the post type will be placed as a sub menu of that.

Note: When using ‘some string’ to show as a submenu of a menu page created by a plugin, this item will become the first submenu item, and replace the location of the top-level link. If this isn’t desired, the plugin that creates the menu page needs to set the add_action priority for admin_menu to 9 or lower.

Note: As this one inherits its value from show_ui, which inherits its value from public, it seems to be the most reliable property to determine, if a post type is meant to be publicly useable. At least this works for _builtin post types and only gives back post and page.

If you look at register_taxonomy(); the parameter accepts a simple true or false but DOESN’T accept a string as an argument for show_in_menu like register_post_type(); does:

show_in_menu
(boolean) (optional) Where to show the taxonomy in the admin menu. show_ui must be true.
Default: value of show_ui argument

‘false’ – do not display in the admin menu
‘true’ – show as a submenu of associated object types

Adding a Custom Post Type to a custom admin menu is simple. Just specify the slug of the custom admin menu as an argument for the show_in_menu parameter while using register_post_type();.

How about adding a Custom Taxonomy to a custom admin menu? That’s a bit tricky to do.

What I’m about to show you works for both Custom Post Types AND Custom Taxonomies.

Step 1 – Register Custom Post Types and Custom Taxonomies

Hook into init and register Custom Post Types and Custom Taxonomies.

if ( ! function_exists( 'tbd_15_init' ) ) {

function tbd_15_init() {

    # Create Custom Post Type

    $post_type_singular = 'MBE Demo Post';
    $post_type_plural   = 'MBE Demo Posts';

    $post_type_args = array(
        'labels'      => array(
            'name'                  => _x( $post_type_plural, 'post type general name', 'mbe' ),
            // General name for the post type, usually plural. The same and overridden by $post_type_object->label. Default is ‘Posts’ / ‘Pages’.
            'singular_name'         => _x( $post_type_singular, 'post type singular name', 'mbe' ),
            // Name for one object of this post type. Default is ‘Post’ / ‘Page’.
            'add_new'               => _x( "Add New {$post_type_singular}", 'setting', 'mbe' ),
            // Default is ‘Add New’ for both hierarchical and non-hierarchical types. When internationalizing this string, please use a gettext context matching your post type. Example: _x( 'Add New', 'product', 'textdomain' );.
            'add_new_item'          => __( "Add New {$post_type_singular}", 'mbe' ),
            // Label for adding a new singular item. Default is ‘Add New Post’ / ‘Add New Page’.
            'edit_item'             => __( "Edit {$post_type_singular}", 'mbe' ),
            // Label for editing a singular item. Default is ‘Edit Post’ / ‘Edit Page’.
            'new_item'              => __( "New {$post_type_singular}", 'mbe' ),
            // Label for the new item page title. Default is ‘New Post’ / ‘New Page’.
            'view_item'             => __( "View {$post_type_singular}", 'mbe' ),
            // Label for viewing a singular item. Default is ‘View Post’ / ‘View Page’.
            'view_items'            => __( "View { $post_type_plural}", 'mbe' ),
            // Label for viewing post type archives. Default is ‘View Posts’ / ‘View Pages’.
            'search_items'          => __( "Search {$post_type_plural}", 'mbe' ),
            // Label for searching plural items. Default is ‘Search Posts’ / ‘Search Pages’.
            'not_found'             => __( "No " . strtolower( $post_type_plural ) . " found.", 'mbe' ),
            // Label used when no items are found. Default is ‘No posts found’ / ‘No pages found’.
            'not_found_in_trash'    => __( "No " . strtolower( $post_type_plural ) . " found in Trash.", 'mbe' ),
            // Label used when no items are in the trash. Default is ‘No posts found in Trash’ / ‘No pages found in Trash’.
            'parent_item_colon'     => __( "Parent {$post_type_singular}:", 'mbe' ),
            // Label used to prefix parents of hierarchical items. Not used on non-hierarchical post types. Default is ‘Parent Page:’.
            'all_items'             => __( "All {$post_type_plural}", 'mbe' ),
            // Label to signify all items in a submenu link. Default is ‘All Posts’ / ‘All Pages’.
            'archives'              => __( "{$post_type_singular} Archives", 'mbe' ),
            // Label for archives in nav menus. Default is ‘Post Archives’ / ‘Page Archives’.
            'attributes'            => __( "{$post_type_singular} Attributes", 'mbe' ),
            // Label for the attributes meta box. Default is ‘Post Attributes’ / ‘Page Attributes’.
            'insert_into_item'      => __( "Insert into {$post_type_singular}", 'mbe' ),
            // Label for the media frame button. Default is ‘Insert into post’ / ‘Insert into page’.
            'uploaded_to_this_item' => __( "Uploaded to this {$post_type_singular}", 'mbe' ),
            // Label for the media frame filter. Default is ‘Uploaded to this post’ / ‘Uploaded to this page’.
            'featured_imaged'       => __( "Featured Image", 'mbe' ),
            // Label for the Featured Image meta box title. Default is ‘Featured Image’.
            'set_featured_image'    => __( "Set Featured Image", 'mbe' ),
            // Label for setting the featured image. Default is ‘Set featured image’.
            'remove_featured_image' => __( "Remove Featured Image", 'mbe' ),
            // Label for removing the featured image. Default is ‘Remove featured image’.
            'use_featured_image'    => __( "Use as Featured Image", 'mbe' ),
            // Label in the media frame for using a featured image. Default is ‘Use as featured image’.
            'menu_name'             => _x( $post_type_plural, 'admin menu', 'mbe' ),
            // Label for the menu name. Default is the same as name.
            'filter_items_list'     => __( "Filter {$post_type_plural} list", 'mbe' ),
            // Label for the table views hidden heading. Default is ‘Filter posts list’ / ‘Filter pages list’.
            'items_list_navigation' => __( "{$post_type_plural} List Navigation", 'mbe' ),
            // Label for the table pagination hidden heading. Default is ‘Posts list navigation’ / ‘Pages list navigation’.
            'items_list'            => __( "{$post_type_plural} List", 'mbe' ),
            // Label for the table hidden heading. Default is ‘Posts list’ / ‘Pages list’.
            'name_admin_bar'        => _x( "Add {$post_type_singular}", 'add new on admin bar', 'mbe' ),
            // String for use in New in Admin menu bar. Default is the same as `singular_name`.
        ),
        'description' => __( '', 'mbe' )
    );

    $post_type_args['public']             = true;
    $post_type_args['publicly_queryable'] = true;
    $post_type_args['show_ui']            = true;
    $post_type_args['show_in_menu']       = false; // Adding Manually later
    $post_type_args['query_var']          = true;

    $post_type_args['has_archive'] = true;

    $post_type_args['hierarchical']  = false;
    $post_type_args['menu_position'] = null;
    $post_type_args['menu_icon']     = null;

    $post_type_args['supports'] = array(
        'title'           => 'title',
        'editor'          => 'editor',
        'thumbnail'       => 'thumbnail',
        'post-thumbnails' => 'post-thumbnails',
        'page-attributes' => 'page-attributes',
        'excerpt'         => 'excerpt'
    );

    $post_type_args['exclude_from_search'] = true;

    $post_type_args['map_meta_cap']    = true;
    $post_type_args['capability_type'] = 'post';

    $post_type_args['capabilities']                           = array(); // To Disallow: 'do_not_allow'
    $post_type_args['capabilities']['edit_post']              = 'edit_post';
    $post_type_args['capabilities']['read_post']              = 'read_post';
    $post_type_args['capabilities']['delete_post']            = 'delete_post';
    $post_type_args['capabilities']['edit_posts']             = 'edit_posts';
    $post_type_args['capabilities']['edit_others_posts']      = 'edit_others_posts';
    $post_type_args['capabilities']['publish_posts']          = 'publish_posts';
    $post_type_args['capabilities']['read_private_posts']     = 'read_private_posts';
    $post_type_args['capabilities']['read']                   = 'read';
    $post_type_args['capabilities']['delete_posts']           = 'delete_posts';
    $post_type_args['capabilities']['delete_private_posts']   = 'delete_private_posts';
    $post_type_args['capabilities']['delete_published_posts'] = 'delete_published_posts';
    $post_type_args['capabilities']['delete_others_posts']    = 'delete_others_posts';
    $post_type_args['capabilities']['edit_private_posts']     = 'edit_private_posts';
    $post_type_args['capabilities']['edit_published_posts']   = 'edit_published_posts';
    $post_type_args['capabilities']['create_posts']           = 'create_posts';

    $post_type_args['taxonomies'] = array();

    register_post_type( 'mbe-demo-post-type', $post_type_args );

    # Create Custom Taxonomy

    $taxonomy_singular = 'MBE Demo Term';
    $taxonomy_plural   = 'MBE Demo Terms';

    $taxonomy_args           = array();
    $taxonomy_args['labels'] = array();

    $taxonomy_args['labels']['name'] = _x( $taxonomy_plural, 'taxonomy general name', 'mbe' );
    /*
     * General name for the taxonomy, usually plural. The same as and overridden by $tax->label.
     * Default is _x( 'Post Tags', 'taxonomy general name' ) or _x( 'Categories', 'taxonomy general name' ).
     * When internationalizing this string, please use a gettext context matching your post type.
     * Example: _x('Writers', 'taxonomy general name');
     */
    $taxonomy_args['labels']['singular_name'] = _x( $taxonomy_singular, 'taxonomy singular name', 'mbe' );
    /*
     * Name for one object of this taxonomy.
     * Default is _x( 'Post Tag', 'taxonomy singular name' ) or _x( 'Category', 'taxonomy singular name' ).
     * When internationalizing this string, please use a gettext context matching your post type.
     * Example: _x('Writer', 'taxonomy singular name');
     */
    $taxonomy_args['labels']['menu_name'] = __( $taxonomy_plural, 'mbe' );
    // the menu name text. This string is the name to give menu items. If not set, defaults to value of name label.
    $taxonomy_args['labels']['all_items'] = __( "All {$taxonomy_plural}", 'mbe' );
    // the all items text. Default is __( 'All Tags' ) or __( 'All Categories' )
    $taxonomy_args['labels']['edit_item'] = __( "Edit {$taxonomy_singular}", 'mbe' );
    // the edit item text. Default is __( 'Edit Tag' ) or __( 'Edit Category' )
    $taxonomy_args['labels']['view_item'] = __( "View {$taxonomy_singular}", 'mbe' );
    // the view item text, Default is __( 'View Tag' ) or __( 'View Category' )
    $taxonomy_args['labels']['update_item'] = __( "Update {$taxonomy_singular}", 'mbe' );
    // the update item text. Default is __( 'Update Tag' ) or __( 'Update Category' )
    $taxonomy_args['labels']['add_new_item'] = __( "Add New {$taxonomy_singular}", 'mbe' );
    // the add new item text. Default is __( 'Add New Tag' ) or __( 'Add New Category' )
    $taxonomy_args['labels']['new_item_name'] = __( "New {$taxonomy_singular} Name", 'mbe' );
    // the new item name text. Default is __( 'New Tag Name' ) or __( 'New Category Name' )
    $taxonomy_args['labels']['parent_item'] = __( "Parent {$taxonomy_singular}", 'mbe' );
    /*
     * the parent item text.
     * This string is not used on non-hierarchical taxonomies such as post tags.
     * Default is null or __( 'Parent Category' )
     */
    $taxonomy_args['labels']['parent_item_colon'] = __( "Parent {$taxonomy_singular}:", 'mbe' );
    // The same as parent_item, but with colon : in the end null, __( 'Parent Category:' )
    $taxonomy_args['labels']['search_items'] = __( "Search {$taxonomy_plural}", 'mbe' );
    // the search items text. Default is __( 'Search Tags' ) or __( 'Search Categories' )
    $taxonomy_args['labels']['popular_items'] = __( "Popular {$taxonomy_plural}", 'mbe' );
    /*
     * the popular items text. This string is not used on hierarchical taxonomies.
     * Default is __( 'Popular Tags' ) or null
     */
    $taxonomy_args['labels']['separate_items_with_commas'] = __( "Separate {$taxonomy_plural} with commas", 'mbe' );
    /*
     * the separate item with commas text used in the taxonomy meta box.
     * This string is not used on hierarchical taxonomies.
     * Default is __( 'Separate tags with commas' ), or null
     */
    $taxonomy_args['labels']['add_or_remove_items'] = __( "Add or remove {$taxonomy_plural}", 'mbe' );
    /*
     * the add or remove items text and used in the meta box when JavaScript is disabled.
     * This string is not used on hierarchical taxonomies.
     * Default is __( 'Add or remove tags' ) or null
     */
    $taxonomy_args['labels']['choose_from_most_used'] = __( "Choose from most used {$taxonomy_plural}", 'mbe' );
    /*
     * the choose from most used text used in the taxonomy meta box.
     * This string is not used on hierarchical taxonomies.
     * Default is __( 'Choose from the most used tags' ) or null
     */
    $taxonomy_args['labels']['not_found'] = __( "No {$taxonomy_plural} found.", 'mbe' );
    /*
     * the text displayed via clicking 'Choose from the most used tags' in the taxonomy meta box when no tags are available
     * and (4.2+) - the text used in the terms list table when there are no items for a taxonomy.
     * Default is __( 'No tags found.' ) or __( 'No categories found.' )
     */

    $taxonomy_args['description'] = __( '', 'mbe' );

    $taxonomy_args['hierarchical']       = true;
    $taxonomy_args['public']             = false;
    $taxonomy_args['publicly_queryable'] = false;
    $taxonomy_args['query_var']          = false;
    $taxonomy_args['show_ui']            = true;
    $taxonomy_args['show_in_menu']       = false; // Adding Manually later
    $taxonomy_args['show_in_nav_menus']  = false;
    $taxonomy_args['show_in_quickedit']  = false;
    $taxonomy_args['show_in_rest']       = false;
    $taxonomy_args['show_tagcloud']      = false;
    $taxonomy_args['show_admin_column']  = true;
    $taxonomy_args['_builtin']           = false;
    $taxonomy_args['sort']               = false;

    $taxonomy_args['update_count_callback'] = '_update_post_term_count';
    $taxonomy_args['meta_box_cb']           = null;

    $taxonomy_args['capabilities']                 = array(); // To Disallow: 'do_not_allow'
    $taxonomy_args['capabilities']['manage_terms'] = 'manage_terms';
    $taxonomy_args['capabilities']['edit_terms']   = 'edit_terms';
    $taxonomy_args['capabilities']['delete_terms'] = 'delete_terms';
    $taxonomy_args['capabilities']['assign_terms'] = 'edit_posts';

    register_taxonomy( 'mbe-demo-taxonomy', 'mbe-demo-post-type', $taxonomy_args );

}

add_action( 'init', 'tbd_15_init' );

}

Step 2 – Create Parent Admin Menu Item and Admin Submenu Items

Hook into admin_menu to create a custom parent admin menu, and add Custom Submenu Admin Pages, Custom Post Type pages, and Custom Taxonomy Pages all to the custom parent admin menu.

if ( ! function_exists( 'tbd_15_add_admin_menus' ) && ! function_exists( 'tbd_15_display_admin_page' ) ) {

function tbd_15_add_admin_menus() {

    # Settings for custom admin menu
    $page_title = 'MBE DEMO';
    $menu_title = 'MBE MENU';
    $capability = 'post';
    $menu_slug  = 'mbe-demo';
    $function   = 'tbd_15_display_admin_page';// Callback function which displays the page content.
    $icon_url   = 'dashicons-admin-page';
    $position   = 0;

    # Add custom admin menu
    add_menu_page( $page_title, $menu_title, $capability, $menu_slug, $function, $icon_url, $position );

    $submenu_pages = array(

        # Avoid duplicate pages. Add submenu page with same slug as parent slug.
        array(
            'parent_slug' => 'mbe-demo',
            'page_title'  => 'MBE Overview',
            'menu_title'  => 'Overview',
            'capability'  => 'read',
            'menu_slug'   => 'mbe-demo',
            'function'    => 'tbd_15_display_admin_page',
            // Uses the same callback function as parent menu.
        ),

        # Post Type :: View All Posts
        array(
            'parent_slug' => 'mbe-demo',
            'page_title'  => '',
            'menu_title'  => 'View Demo Posts',
            'capability'  => '',
            'menu_slug'   => 'edit.php?post_type=mbe-demo-post-type',
            'function'    => null,// Doesn't need a callback function.
        ),

        # Post Type :: Add New Post
        array(
            'parent_slug' => 'mbe-demo',
            'page_title'  => '',
            'menu_title'  => 'Add Demo Post',
            'capability'  => '',
            'menu_slug'   => 'post-new.php?post_type=mbe-demo-post-type',
            'function'    => null,// Doesn't need a callback function.
        ),

        # Taxonomy :: Manage Terms
        array(
            'parent_slug' => 'mbe-demo',
            'page_title'  => '',
            'menu_title'  => 'MBE Demo Terms',
            'capability'  => '',
            'menu_slug'   => 'edit-tags.php?taxonomy=mbe-demo-taxonomy&post_type=mbe-demo-post-type',
            'function'    => null,// Doesn't need a callback function.
        ),

    );

    # Add each submenu item to custom admin menu.
    foreach ( $submenu_pages as $submenu ) {

        add_submenu_page(
            $submenu['parent_slug'],
            $submenu['page_title'],
            $submenu['menu_title'],
            $submenu['capability'],
            $submenu['menu_slug'],
            $submenu['function']
        );

    }

}

add_action( 'admin_menu', 'tbd_15_add_admin_menus', 1 );

/* If you add any extra custom sub menu pages which are not a Custom Post Type or a Custom Taxonomy, you will need
 * to create a callback function for each of your custom submenu items you create above.
 */

# Default Admin Page for Custom Admin Menu
function tbd_15_display_admin_page() {

    # Display custom admin page content from newly added custom admin menu.
    echo '<div class="wrap">' . PHP_EOL;
    echo '<h2>My Custom Admin Page Title</h2>' . PHP_EOL;
    echo '<p>This is the custom admin page created from the custom admin menu.</p>' . PHP_EOL;
    echo '</div><!-- end .wrap -->' . PHP_EOL;
    echo '<div class="clear"></div>' . PHP_EOL;

}

}

Step 3 – Highlight Appropriate Admin Submenu Item

Hook into parent_file to correctly highlight your Custom Post Type and Custom Taxonomy submenu items with your custom parent menu/page.

if ( ! function_exists( 'tbd_15_set_current_menu' ) ) {

function tbd_15_set_current_menu( $parent_file ) {

    global $submenu_file;

    $current_screen = get_current_screen();

    # Set the submenu as active/current while anywhere in your Custom Post Type
    if ( $current_screen->post_type == 'mbe-demo-post-type' ) {

        if ( $current_screen->base == 'post' ) {
            $submenu_file = 'edit.php?post_type=' . $current_screen->post_type;
        }

        if ( $current_screen->base == 'post' && $current_screen->action == 'add' ) {
            $submenu_file = 'post-new.php?post_type=' . $current_screen->post_type;
        }

        if ( $current_screen->base == 'edit-tags' && $current_screen->taxonomy == 'mbe-demo-taxonomy' ) {
            $submenu_file = 'edit-tags.php?taxonomy=mbe-demo-taxonomy&post_type=' . $current_screen->post_type;
        }

        $parent_file = 'mbe-demo';

    }

    return $parent_file;

}

add_filter( 'parent_file', 'tbd_15_set_current_menu' );

}

If you need any clarification about how any of this works, read the following pages from top to bottom:

  1. Adding Custom Parent Admin Menus
  2. Adding Custom Child Admin Menus
  3. Roles and Capabilities in WordPress
  4. Registering Custom Post Types
  5. Registering Custom Taxonomies
  6. WordPress Plugin API :: Action Reference
  7. WordPress Plugin API :: Action Reference :: init
  8. WordPress Plugin API :: Action Reference :: admin_menu
  9. WordPress Plugin API :: Filter Reference
  10. List of All WordPress Hooks (including actions and filters)

Credits

Original question asked by @numediaweb on WordPress Stack Exchange: Show custom taxonomy inside custom menu.
Answer to Question: Show custom taxonomy inside custom menu by @Michael Ecklund on WordPress Stack Exchange.

Related Questions

This article assists in answering all of the following related questions:

  1. [WordPress Development] Show custom taxonomy inside custom menu asked by: @numediaweb
  2. [WordPress Development] How do I create multiple post types in same menu section in WP-admin? asked by: @Peter Westerlund
  3. [WordPress Development] Show custom taxonomies in admin panel under custom post type asked by: @Eoghan OLoughlin
  4. [WordPress Development] How to add a taxonomy into admin menu asked by: @genuy11512

14 COMMENTS

  1. I’m unable to get your code working in the newest version of WordPress. I’ve even tried copying it exactly from your page into a new plugin called news/news.php and activating it and nothing. It doesn’t show any new menus on the admin menu.

  2. Thanks for this tutorial! I have 14 custom post types and each requires documentation and has custom settings so this really helped me to get all of the in line and displayed perfectly.

    The issue I’m having is with step three. On menus where I have several taxonomies, I can only get the last taxonomy added to the function to be highlighted.

    I tried altering the if($pagenow==edit_tags.php) line to include the specific taxonomy and post type but then none of the taxonomies are highlighted.

    I also tried adding the additional taxonomies as additional lines in the if statement but tstill only the last taxonomy added is highlighted.

    The documentation on parent_file in the codex is pretty vague.

    Is this a case where I would use an ifelse or a switch statement?

    Any help would be appreciated!

    Thanks!

    • I was able to solve this but perhaps there is a more elegant solution.

      I simple wrapped each if ( $pagenow == 'edit-tags.php' ) statement in an additional check using: if ( $current_screen->taxonomy == 'MY_TAXONOMY' ).

      This allows multiple taxonomies to be highlighted as they are displayed in the admin section.

      Let me know if you can think of an easier or simpler way to do this.

      Thanks so much!

    • You’re very welcome! Thank you for the comment(s). Yes, you’re on the right track. You’ll need to specify detailed conditions to match your taxonomy appropriately. You could try using get_query_var( 'taxonomy' ); or if that doesn’t work, you could use $_GET[ 'taxonomy' ]; You could also investigate the WP_Screen object using get_current_screen(); or global $current_screen.

  3. Hi Michael,

    Is there anyway to have only one taxonomy admin panel between custom post types?

    For example, my blog posts and my CPT share the same two taxonomies (location and niches). The list of locations and niches are going to be the same for both blog posts and the CPT. So is there anyway I can have just one place to add locations and niches instead of having multiple taxonomy admin panels?

    Thanks,
    Roselle

    • That’s essentially what this article is meant to cover.

      To quickly recap:

      1. Step 1: Register Custom Post Types & Custom Taxonomies. During registration, specify `show_in_menu => false`.
      2. Step 2: Add parent menu add_menu_page(), and/or add submenus add_submenu_page().
      3. Step 3: If a submenu was added to any parent menu, other than the default. The highlighting and menu expansion most likely won’t work. This needs to be corrected using the filter `parent_file`.

      Everything you need to do is outlined above the comments in my article. If there’s anything which is unclear about my article, please let me know so I can clear that up for you and/or anyone else who may feel the same.

  4. My website has so many taxonomy, can you help me show only one taxonomy selected on wp admin edit post ?

    current I set hidden taxonomy with `show_ui` is `false`.

    register_taxonomy('developer', 'post', array(
    'hierarchical' > true,
    'labels' > $labels, /* NOTICE: Here is where the $labels variable is used */
    'show_ui' > FALSE,
    'query_var' > true,
    'rewrite' > array('slug' > 'developer'),
    ));

    • @thoman

      I would suggest you get familiar with the following areas of WordPress Development:

      Once you’ve familiarized yourself with these areas of WordPress development; I encourage you to come back to this post and read it again.

      This article simply aims at controlling which menus/submenus your custom Taxonomy pages and custom Post Type pages are displayed (Rather than the default behavior of a standalone parent menu for Post Types and/or associated child menu for Taxonomies.). This is particularly useful if you have a plugin which creates it’s own administration menu, and you want your Post Types and/or Taxonomies listed under the same administration menu.

  5. THANK YOU! I have been searching and searching as to why custom taxonomies would not display in my custom admin menus when it is so easy with custom post types. Thanks for your solution!

Leave a Reply