Every theme and plugin developer eventually faces the task of adding custom pages to the WordPress admin dashboard. The WordPress Administration Menu API provides a standardized way to integrate your interface into the admin panel without breaking compatibility with the core or other plugins. This guide covers every aspect of menu creation from basic structure to advanced placement strategies. By the end, you will know exactly how to build professional admin interfaces that feel native to WordPress.
Understanding the Admin Menu Architecture
WordPress provides two primary functions for building admin menus: add_menu_page() for top-level menu items and add_submenu_page() for child entries that appear under a parent. Both functions hook into admin_menu, which fires after the basic admin panel structure has been initialized but before menus are rendered in the sidebar. This timing is deliberate: it gives you a window to register your menus before WordPress draws the navigation.
The menu system in WordPress has a defined hierarchy. Top-level menus appear in the left sidebar of the dashboard. Each top-level menu can have multiple submenu items. WordPress core menus occupy positions 2 through 10 by default for Dashboard, Posts, Media, Pages, Comments, Appearance, Plugins, Users, Tools, and Settings. Position 1 is reserved for the Dashboard separator. When you create a custom menu, you are inserting into this ordered list alongside core items.
Behind the scenes, WordPress stores menu data in the global $menu and $submenu arrays. Each entry is an associative array keyed by menu slug. The $menu array holds top-level items as numerically indexed arrays where each element is [menu_title, capability, menu_slug, page_title, css_classes, hookname, icon_url]. Understanding this structure helps when debugging menu conflicts or writing code that modifies menus registered by other plugins.
add_menu_page(): Creating Top-Level Menus
The add_menu_page() function accepts up to 7 arguments. Five are mandatory, two are optional but essential for proper placement and branding. The function signature is well-documented but frequently misused because developers skip reading the parameter descriptions:
add_menu_page(
string $page_title,
string $menu_title,
string $capability,
string $menu_slug,
callable $callback = '',
string $icon_url = '',
int $position = null
);
[/codeblock]
| Parameter | Type | Required | Description |
|---|---|---|---|
| $page_title | string | Yes | Text displayed in the browser title bar when viewing the admin page. Use something descriptive like "MyTheme Settings". This also appears as the h1 heading on your page if you use the standard WordPress admin markup. |
| $menu_title | string | Yes | Text shown in the admin sidebar menu. Keep it short. Long titles break the sidebar layout and get truncated by CSS. Maximum recommended length is about 20 characters including spaces. |
| $capability | string | Yes | Required user capability to access the page. Use 'manage_options' for general settings, 'edit_theme_options' for theme-specific settings. WordPress checks this against the current user before showing the menu item. |
| $menu_slug | string | Yes | Unique identifier for the menu page, used in URL as ?page=your-slug. Prefix with your plugin or theme namespace to avoid collisions. Never use generic slugs like "settings" or "options". |
| $callback | callable | Yes | Function that renders the page content. Must output valid HTML. If omitted or set to falsy, WordPress assumes you will handle page rendering through other hooks, which is an advanced pattern rarely used in practice. |
| $icon_url | string | No | Menu icon. Accepts Dashicons class name (e.g., 'dashicons-admin-generic'), SVG data URI, or URL to an image file. Using a Dashicon class is the simplest approach and ensures your icon matches the WordPress admin color scheme. |
| $position | int | No | Position in the sidebar menu order. Higher numbers appear lower. Core menus occupy positions 2-10, 15, 20, 25, 60, 65, 70, 75, 80. Decimals like 60.5 are not supported—use whole integers only. |
Practical Example: Theme Options Page
Here is a complete example that registers a top-level menu for a theme settings page. Notice the use of Dashicons for the icon and position 61 to place it immediately after the Appearance menu:
function mytheme_add_admin_menu() {
add_menu_page(
'MyTheme Settings',
'MyTheme',
'manage_options',
'mytheme-settings',
'mytheme_settings_page_callback',
'dashicons-admin-generic',
61
);
}
add_action('admin_menu', 'mytheme_add_admin_menu');
function mytheme_settings_page_callback() {
?>
MyTheme Settings
Configure your theme options here.
Understanding the Capability Parameter
The capability parameter controls who can see and access your menu pages. This is not just a cosmetic choice — it is your primary access control mechanism in the admin panel. WordPress provides a hierarchy of built-in capabilities mapped to user roles. Choosing the wrong capability can expose sensitive settings to editors who should not see them, or hide important options from administrators who need them.
| Role | Typical Capabilities | Use Case for Menu Access |
|---|---|---|
| Super Admin | manage_network, all others | Multisite network settings, user management across sites, domain mapping |
| Administrator | manage_options, activate_plugins, edit_theme_options, install_plugins | General plugin settings, theme options, site-wide configuration, import/export tools |
| Editor | edit_pages, edit_others_posts, manage_categories | Content management menus, category and tag configuration, post moderation dashboards |
| Author | edit_published_posts, upload_files | Personal content dashboards, media upload interfaces, profile customization |
| Contributor | edit_posts, delete_posts | Minimal. Contributors rarely have dedicated menus beyond the default Posts screen |
| Subscriber | read | Profile customization pages only. Subscribers typically only see their own profile editing screen |
A common mistake is using 'manage_options' for menus that should be accessible to editors. If your menu controls content-related settings like default category or comment moderation, use 'edit_pages' or 'moderate_comments' instead. This gives the right people access while keeping site configuration options guarded behind 'manage_options'.
add_submenu_page(): Building Menu Hierarchies
The add_submenu_page() function creates an entry that appears under an existing top-level menu. The signature is almost identical to add_menu_page() but with one additional parameter—the parent menu slug—and it does not accept icon or position arguments since those are inherited from the parent:
add_submenu_page(
string $parent_slug,
string $page_title,
string $menu_title,
string $capability,
string $menu_slug,
callable $callback = ''
);
[/codeblock]
The $parent_slug parameter determines where your submenu appears. Pass the slug of your own top-level menu to nest submenus under your custom menu. Pass a core WordPress slug to add your page under an existing menu like Settings or Tools. Both approaches are valid and widely used depending on the context of what your page does.
Adding Submenus Under a Custom Top-Level Menu
When using a top-level menu you created, reference your own menu slug as the parent. This is the standard pattern for plugins and themes that need multiple configuration pages organized under one parent:
function mytheme_add_admin_submenus() {
add_submenu_page(
'mytheme-settings',
'Social Network Options',
'Social Networks',
'manage_options',
'mytheme-social',
'mytheme_social_page_callback'
);
add_submenu_page(
'mytheme-settings',
'Display Options',
'Display',
'manage_options',
'mytheme-display',
'mytheme_display_page_callback'
);
}
add_action('admin_menu', 'mytheme_add_admin_submenus');
[/codeblock]
Adding Submenus Under Existing WordPress Menus
You can also add submenu pages to built-in WordPress menus. This is common for plugins that want their settings available under the Settings menu rather than creating their own top-level entry. Use the standard core slugs:
function myplugin_add_under_settings() {
add_submenu_page(
'options-general.php',
'API Configuration',
'API Settings',
'manage_options',
'myplugin-api-config',
'myplugin_api_config_callback'
);
}
add_action('admin_menu', 'myplugin_add_under_settings');
[/codeblock]
| Core Menu | Parent Slug | Typical Submenu Use |
|---|---|---|
| Dashboard | index.php | Custom analytics widgets, activity feeds |
| Posts | edit.php | Custom post status screens, editorial calendars |
| Media | upload.php | Media library extensions, CDN management |
| Pages | edit.php?post_type=page | Page templates, bulk page editors |
| Appearance | themes.php | Custom CSS editors, font management |
| Plugins | plugins.php | Plugin dependency checkers, bulk management |
| Users | users.php | User profile extensions, import/export tools |
| Tools | tools.php | Diagnostic tools, data import/export utilities |
| Settings | options-general.php | Plugin-specific configuration pages |
Menu Placement Strategies
Where you place your menu affects discoverability and perceived importance. WordPress uses a numeric position system where lower numbers appear higher in the sidebar. Choosing the right position is part UX decision and part convention. Core positions are:
2 — Dashboard
4 — Separator
5 — Posts
10 — Media
15 — Links
20 — Pages
25 — Comments
59 — Separator
60 — Appearance
65 — Plugins
70 — Users
75 — Tools
80 — Settings
99 — Separator
[/codeblock]
For theme-specific settings, position 61 places your menu between Appearance and Plugins, which is where users intuitively look for theme-related options. For plugin-specific menus, position 66 places you just below Plugins. For utilities and tools, positions 76-79 sit between Tools and Settings. The gaps between core positions are intentional, giving you room to insert custom menus without overriding built-in items.
Complete Implementation: Theme Options with Submenus
Below is a production-ready implementation that creates a top-level menu with three submenus for a theme. This pattern uses the Settings API for proper option handling, demonstrates the slug-sharing technique to prevent duplicate submenu entries, and includes capability checks in every callback for defense in depth:
function mytheme_admin_menu_setup() {
add_menu_page(
'MyTheme Settings',
'MyTheme',
'manage_options',
'mytheme-main',
'mytheme_main_page_html',
'dashicons-admin-customizer',
61
);
add_submenu_page(
'mytheme-main',
'General Settings',
'General',
'manage_options',
'mytheme-main',
'mytheme_main_page_html'
);
add_submenu_page(
'mytheme-main',
'Social Network Settings',
'Social Networks',
'manage_options',
'mytheme-social',
'mytheme_social_page_html'
);
add_submenu_page(
'mytheme-main',
'Display Settings',
'Display',
'manage_options',
'mytheme-display',
'mytheme_display_page_html'
);
add_submenu_page(
'mytheme-main',
'Typography Settings',
'Typography',
'manage_options',
'mytheme-typography',
'mytheme_typography_page_html'
);
}
add_action('admin_menu', 'mytheme_admin_menu_setup');
function mytheme_main_page_html() {
if (!current_user_can('manage_options')) {
return;
}
?>
MyTheme General Settings
Removing and Reordering Existing Menus
Sometimes you need to remove core menus for certain user roles or reorganize the admin sidebar. WordPress provides remove_menu_page() for top-level menus and remove_submenu_page() for child entries. Both should be hooked into admin_menu with a high priority to ensure they execute after the target menus have been registered:
function mytheme_clean_admin_menu() {
if (!current_user_can('manage_options')) {
remove_menu_page('tools.php');
remove_menu_page('edit-comments.php');
}
remove_submenu_page('themes.php', 'theme-editor.php');
remove_submenu_page('plugins.php', 'plugin-editor.php');
}
add_action('admin_menu', 'mytheme_clean_admin_menu', 999);
[/codeblock]
Dynamic Menu Visibility Based on User Context
You can conditionally register menus based on user capabilities, site configuration, or even the current screen. This allows you to show menus only when they are relevant, reducing clutter for users who do not need certain options:
function mytheme_conditional_menus() {
if (current_user_can('manage_options')) {
add_menu_page(
'Advanced Settings',
'Advanced',
'manage_options',
'mytheme-advanced',
'mytheme_advanced_page'
);
}
if (get_option('mytheme_enable_analytics')) {
add_submenu_page(
'mytheme-main',
'Analytics Dashboard',
'Analytics',
'manage_options',
'mytheme-analytics',
'mytheme_analytics_page'
);
}
$active_plugins = get_option('active_plugins');
if (in_array('woocommerce/woocommerce.php', $active_plugins)) {
add_submenu_page(
'mytheme-main',
'WooCommerce Integration',
'WooCommerce',
'manage_options',
'mytheme-woocommerce',
'mytheme_woocommerce_page'
);
}
}
add_action('admin_menu', 'mytheme_conditional_menus');
[/codeblock]
The third example is particularly useful: it only shows the WooCommerce integration submenu if WooCommerce is actually installed and active. This prevents confusing users with options for plugins they do not have.
Using SVG Icons for Menus
While Dashicons work well for standard cases, custom SVG icons provide a more distinctive appearance and scale perfectly on high-DPI displays. The SVG should be encoded as a base64 data URI and use fill="%23999" to match the default WordPress admin color, which core CSS recolors to blue on hover and active states:
function mytheme_custom_icon() {
$svg = '';
$icon = 'data:image/svg+xml;base64,' . base64_encode($svg);
add_menu_page(
'MyTheme',
'MyTheme',
'manage_options',
'mytheme-main',
'mytheme_main_page_html',
$icon,
61
);
}
add_action('admin_menu', 'mytheme_custom_icon');
[/codeblock]
The fill color uses the WordPress admin gray (#999) by default. The core CSS automatically recolors icons on hover: when the menu item is active or hovered, WordPress applies a blue tint through CSS filters. The %23 in the SVG fill is the URL-encoded hash character required because the color value is embedded inside a data URI.
Internationalization of Menu Labels
All menu titles and page titles should pass through translation functions for multilingual support. WordPress uses the __() and _x() functions with a textdomain matching your plugin or theme slug. Load translations before the admin_menu hook fires:
function mytheme_i18n_menu() {
add_menu_page(
__('MyTheme Settings', 'mytheme'),
__('MyTheme', 'mytheme'),
'manage_options',
'mytheme-main',
'mytheme_main_page_html'
);
add_submenu_page(
'mytheme-main',
__('Social Network Options', 'mytheme'),
__('Social Networks', 'mytheme'),
'manage_options',
'mytheme-social',
'mytheme_social_page_html'
);
}
add_action('admin_menu', 'mytheme_i18n_menu');
[/codeblock]
The Problem of Duplicate Submenu Entries
One of the most common frustrations when building admin menus is discovering that WordPress has created a duplicate submenu entry with the same name as your top-level menu. This happens because WordPress auto-generates a submenu item for every top-level menu that has at least one submenu registered. The auto-generated entry uses the top-level menu's title and slug.
The fix is simple: make your first add_submenu_page() call use the exact same $menu_slug as the parent add_menu_page(). This replaces the auto-generated entry with your custom-labeled one. Here is a before-and-after comparison:
Without the fix (duplicate appears):
add_menu_page('Theme Options', 'MyTheme', 'manage_options', 'mytheme', 'callback');
// This creates a duplicate "MyTheme" submenu!
add_submenu_page('mytheme', 'General', 'General', 'manage_options', 'mytheme-general', 'callback');
[/codeblock]
With the fix (clean menu):
add_menu_page('Theme Options', 'MyTheme', 'manage_options', 'mytheme', 'callback');
// First submenu uses same slug — replaces auto-generated entry
add_submenu_page('mytheme', 'General', 'General', 'manage_options', 'mytheme', 'callback');
add_submenu_page('mytheme', 'Social', 'Social', 'manage_options', 'mytheme-social', 'callback');
[/codeblock]
Debugging Menu Registration Issues
When menus do not appear as expected, systematic debugging saves hours. Here are the most common failure points and how to diagnose them:
Check if the hook fires
Add a temporary error_log() call inside your admin_menu callback to verify it executes. If nothing appears in the debug log, your add_action() call is likely placed after the hook fires or contains a typo in the function name.
Check capability matching
The capability you pass to add_menu_page() must match what the current user actually has. If you are logged in as Administrator and use 'edit_published_posts', the menu will appear. If you later test as Editor and use 'manage_options', it will not. Use the User Switching plugin to test different roles quickly.
Check slug conflicts
Two menus cannot share the same slug. If another plugin registered 'mytheme-settings' before your code runs, your add_menu_page() will fail silently. Always prefix slugs with your plugin or theme namespace and use unique, specific identifiers.
Check callback existence
If the callback function does not exist when add_menu_page() is called, the menu is registered but clicking it produces a fatal error. Verify the function name is spelled correctly and the function is defined in a file that has been included before admin_menu fires.
Working with Custom Post Type Menus
Custom post types registered with 'show_in_menu' => true automatically get a top-level menu and submenus for listing, adding, and managing taxonomy terms. You can add your own submenus to these CPT menus by using the post type's menu slug:
function myplugin_add_cpt_submenu() {
add_submenu_page(
'edit.php?post_type=product',
'Product Import',
'Import Products',
'manage_options',
'product-import',
'myplugin_product_import_page'
);
}
add_action('admin_menu', 'myplugin_add_cpt_submenu');
[/codeblock]
The slug format for custom post type menus is edit.php?post_type={post_type_key}. This pattern works for any post type registered with a non-reserved key, including those from WooCommerce (product, shop_order), Events Calendar (tribe_events), or your own custom post types.
Performance Considerations
While menu registration is lightweight, page rendering callbacks are not. Here are guidelines for keeping your admin pages fast:
- Do not load all of WordPress on every admin page. Use get_current_screen() inside your admin_menu callback to check if you are on the right page before loading heavy dependencies like the WordPress media uploader, code editors, or third-party SDKs.
- Enqueue admin scripts and styles only on your specific pages using the admin_enqueue_scripts hook with the $hook_suffix parameter, not globally on every admin page load.
- Cache expensive database queries in transients for admin pages that pull large datasets like logs, analytics, or import histories.
- Use admin-ajax.php or the REST API for data that updates dynamically rather than reloading the full page with every interaction.
Frequently Asked Questions
What is the difference between add_menu_page() and add_submenu_page()?
add_menu_page() creates a top-level entry in the admin sidebar with its own icon and position. add_submenu_page() creates a child entry that appears under a parent menu, whether that parent is a core WordPress menu or a custom one you created. The main difference is that add_submenu_page() requires a $parent_slug parameter to define which menu it belongs to, and it does not accept icon or position arguments since those are inherited from the parent. Use add_menu_page() when you need a new top-level section; use add_submenu_page() to organize related settings under a common parent.
What hook should I use to register admin menus?
Use the admin_menu action hook. It fires after the basic admin panel structure is set up but before menus are rendered in the sidebar. This is the only hook you need for menu registration. If you need to modify menus registered by other plugins or themes, use a low priority value such as 999 to ensure your callback runs after all other plugins have registered their menus. For removing menus based on user role, consider also hooking into admin_init to check the current user context.
Which capability should I use for theme settings menus?
For theme-specific settings that control appearance and layout, use edit_theme_options. This capability is granted to Administrators by default and is the standard for theme customization pages. For plugin settings that affect site-wide configuration like API keys, caching, or security settings, use manage_options. For content-related menus like editorial calendars or custom taxonomy management, use edit_posts or edit_pages depending on the required access level.
How do I place a menu between two existing WordPress menus?
Use the $position argument of add_menu_page() with a value between two core menu positions. Core menus use positions 2 (Dashboard), 5 (Posts), 10 (Media), 20 (Pages), 25 (Comments), 60 (Appearance), 65 (Plugins), 70 (Users), 75 (Tools), and 80 (Settings). To place a menu between Appearance and Plugins, use position 61, 62, 63, or 64. To place between Plugins and Users, use 66-69. WordPress automatically handles collisions by incrementing duplicate positions, so you can safely use any integer in the gap.
Why does my menu show a duplicate submenu entry?
WordPress automatically creates a submenu entry with the top-level menu title when no submenu shares the parent menu slug. To prevent this, make your first add_submenu_page() call use the same $menu_slug as the parent add_menu_page(). This replaces the auto-generated entry with your custom-labeled one. If the duplicates persist, check that you are not registering the same menu twice in different hook callbacks.
Can I add submenus to core WordPress menus?
Yes. Pass the core menu slug as the $parent_slug parameter. Common slugs include index.php (Dashboard), edit.php (Posts), themes.php (Appearance), plugins.php (Plugins), options-general.php (Settings), and tools.php (Tools). This is a common pattern for adding plugin configuration pages under the Settings menu. For custom post type menus, use the format edit.php?post_type={post_type_key}.
How do I remove an existing admin menu?
Use remove_menu_page('slug') for top-level menus and remove_submenu_page('parent-slug', 'submenu-slug') for submenus. Hook into admin_menu with a high priority number, typically 999, to ensure your removal runs after the target menu has been registered. For role-specific removals, conditionally call these functions inside your admin_menu callback based on current_user_can() checks.
What icon options are available for admin menus?
Three options: Dashicons CSS classes (e.g., 'dashicons-admin-generic'), which are included in WordPress core and support all built-in icon glyphs; custom image URLs pointing to a 20x20px PNG or other image format; and base64-encoded SVG data URIs for resolution-independent icons that scale on high-DPI displays. SVGs are preferred for modern development because they scale perfectly, can be colored to match the admin theme via the fill attribute, and have zero HTTP requests compared to image URLs. Use fill="%23999" in the SVG to match the default WordPress admin color scheme.
Tap to react



