Sometimes a plugin or theme needs a new page in the admin area, this is post will explain how to do this.
There are a number of functions that can be called to make this happen, but they all use add_menu_page or add_submenu_page. Want to see how this all fits together? The code is on github.
The overview of this is that a function that calls add_menu_page, add_submenu_page, one of the other functions described below must be hooked into the admin_menu hook.
So in a normally bootstrapped WordPress plugin, that might look like this.
// hooked into `plugins_loaded` in the main plugin file
function chrisguitarguy_adminpages_load()
{
add_action('admin_menu', 'chrisguitarguy_adminpages_add');
}
function chrisguitarguy_adminpages_add()
{
// call `add_menu_page` or `add_submenu_page`, etc
}
Adding a Top Level Menu Page
This is done with add_menu_page.
// hooked into `admin_menu`, see above
function chrisguitarguy_adminpages_add()
{
add_menu_page(
// page <title> tag, localize it like any other user-facing string
__('Example Top-Level Page', 'chrisguitarguy-adminpages'),
// text displayed in the menu, localize it
__('Example Top', 'chrisguitarguy-adminpages'),
// what capability is required to access this? `manage_options` would be
// an admin-level user.
'manage_options',
// page url slug, prefix like anything else
'chrisguitarguy-adminpages-toplevel',
// callback, this is called to display the actual page
'chrisguitarguy_adminpages_callback',
// this is the icon URL, we'll skip this, but it can be used to
// show a custom ICON in the menu.
'',
// position, where in the menu should this page be displayed?
// higher number === lower on the page, can use ints or floats here
1000
);
}
// show the page
function chrisguitarguy_adminpages_callback()
{
include __DIR__.'/../views/page.php';
}
Here’s an illustration of where all those values passed end up displayed on the page.

On the Admin Page $function
This callable is invoked by WordPress to actually show the page. I like to keep the actual HTML itself in a separate view file.
Whatever the organization, the callback should do two things to conform to the style of the rest of the WordPress admin.
- Wrap the entire admin page with a
<div class="wrap">. - Use an
<h1>to show the page title.
<?php
// in views/page.php
// exit if WordPress isn't loaded
!defined('ABSPATH') && exit;
?>
<div class="wrap">
<h1>Admin Page Example</h1>
<?php /* other things here */ ?>
</div>
Adding Submenu Pages
Call add_submenu_page instead of add_menu_page.
// hooked into `admin_menu`, see above
function chrisguitarguy_adminpages_add()
{
add_submenu_page(
// the page under which the submenu page should be nested
'options-general.php',
// page <title> tag, localize it like any other user-facing string
__('Example Submenu Page', 'chrisguitarguy-adminpages'),
// text displayed in the menu, localize it
__('Example Submenu', 'chrisguitarguy-adminpages'),
// what capability is required to access this? `manage_options` would be
// an admin-level user.
'manage_options',
// page url slug, prefix like anything else
'chrisguitarguy-adminpages-submenu',
// callback, this is called to display the actual page
'chrisguitarguy_adminpages_callback'
);
}
Here’s an illustration of where all those variables end up.

There are quite a few other functions that are small wrappers around add_submenu_page that add pages under specific places in the admin area.
function chrisguitarguy_adminpages_add()
{
// Under Tools
add_management_page(
__('Example Management Page', 'chrisguitarguy-adminpages'),
__('Example Manage', 'chrisguitarguy-adminpages'),
'manage_options',
'chrisguitarguy-adminpages-management',
'chrisguitarguy_adminpages_callback'
);
// Under Settings
add_options_page(
__('Example Options Page', 'chrisguitarguy-adminpages'),
__('Example Options', 'chrisguitarguy-adminpages'),
'manage_options',
'chrisguitarguy-adminpages-options',
'chrisguitarguy_adminpages_callback'
);
// Under Appearance
add_theme_page(
__('Example Theme Page', 'chrisguitarguy-adminpages'),
__('Example Theme', 'chrisguitarguy-adminpages'),
'manage_options',
'chrisguitarguy-adminpages-theme',
'chrisguitarguy_adminpages_callback'
);
// Under Themes
add_plugins_page(
__('Example Plugin Page', 'chrisguitarguy-adminpages'),
__('Example Plugin', 'chrisguitarguy-adminpages'),
'manage_options',
'chrisguitarguy-adminpages-plugin',
'chrisguitarguy_adminpages_callback'
);
// Under Users
add_users_page(
__('Example User Page', 'chrisguitarguy-adminpages'),
__('Example User', 'chrisguitarguy-adminpages'),
'manage_options',
'chrisguitarguy-adminpages-user',
'chrisguitarguy_adminpages_callback'
);
}

Adding Submenus Under Post Types
Submenu pages can be added under post types by using edit.php?post_type={posttype} as the $parent_slug argument.
To add a submenu page under the Pages area, for example:
function chrisguitarguy_adminpages_add()
{
// Under Pages
add_submenu_page(
'edit.php?post_type=page',
__('Example Post Type Page', 'chrisguitarguy-adminpages'),
__('Example Type', 'chrisguitarguy-adminpages'),
'manage_options',
'chrisguitarguy-adminpages-type',
'chrisguitarguy_adminpages_callback'
);
}

Permission Errors on Admin Pages
If any hook other than one of the admin_menu hooks is used to add the menu pages, a “Sorry, you are not allowed to access this page” message will be show.

This is broken:
function chrisguitarguy_adminpages_load()
{
add_action('admin_init', 'chrisguitarguy_adminpages_add');
}
function chrisguitarguy_adminpages_add()
{
// ...
}
Always be sure to hook into admin_menu or one of the other hooks described below.
- add_action('admin_init', 'chrisguitarguy_adminpages_add');
+ add_action('admin_menu', 'chrisguitarguy_adminpages_add');
Other Hooks for Adding Menu Pages
One of three hooks may be used to add menu page.
admin_menu— what we’ve used here, this one always fires in single-site admin areas.network_admin_menu— fires in WordPress multi-site’s network admin area.user_admin_menu— fires in WordPress multi-site’s user admin area (/wp-admin/user/).
Note that these hooks are exclusive. admin_menu will never fire on a network admin page, so choose the one that makes the most sense for the use case.