wppaste
WordPress

do_action( 'admin_menu', string $context )

Since
1.5.0

Register admin menu and submenu pages from a callback on this action, which fires once the admin is loaded and the current user's capabilities are known. It is the only safe place to call add_menu_page() and add_submenu_page(): earlier and the capability checks are not ready, later and the menu has already been rendered. Priority matters more here than almost anywhere else in WordPress, because removing another plugin's menu item requires running after it added one.

Fires before the administration menu loads in the admin.

Compatibility

WordPress
since 1.5.0
  • 6.7.7
  • 6.8.8
  • 6.9.7
  • 7.0.4
  • 7.1.0

Present in every tracked release (6.7.7 to 7.1.0).

Parameters

$contextstring
Empty context.

Code examples

Every example is editable and runs in a real WordPress booted in your browser by WordPress Playground. Press Run, then edit the code: clicking away re-runs it. Nothing is sent anywhere until you do.

Add a top-level admin menu page

The callback runs inside the admin, where add_menu_page() registers both the menu item and the screen that renders when it is clicked.

add_action( 'admin_menu', function () {
	add_menu_page(
		'Field Notes',                 // page <title>
		'Field Notes',                 // menu label
		'manage_options',              // capability required
		'field-notes',                 // menu slug, becomes ?page=field-notes
		'wppaste_render_field_notes',  // renders the screen
		'dashicons-clipboard',
		25                             // position, below Comments
	);
} );

function wppaste_render_field_notes() {
	if ( ! current_user_can( 'manage_options' ) ) {
		wp_die( esc_html__( 'You are not allowed to view this page.' ) );
	}

	echo '<div class="wrap"><h1>Field Notes</h1>';
	echo '<p>Look at the left-hand menu: this item was added by the snippet.</p>';
	echo '</div>';
}

Check the capability again inside the render callback: the menu being hidden is not access control.

Add a page under an existing menu

Most plugins do not need a top-level item; a submenu under Settings is less intrusive and easier for people to find.

add_action( 'admin_menu', function () {
	// Settings > Field Notes. The parent slug is the parent's file name.
	add_submenu_page(
		'options-general.php',
		'Field Notes Settings',
		'Field Notes',
		'manage_options',
		'field-notes-settings',
		function () {
			echo '<div class="wrap"><h1>Field Notes Settings</h1>';
			echo '<p>Open Settings in the sidebar to find this page.</p></div>';
		}
	);
} );

add_options_page() is a thin wrapper around this with the parent slug filled in.

Remove a menu item another plugin added

Removal has to happen after the item exists, which is what the priority argument is for.

// Something else registers a menu at the default priority of 10.
add_action( 'admin_menu', function () {
	add_menu_page( 'Noisy Plugin', 'Noisy Plugin', 'manage_options', 'noisy-plugin', '__return_null', 'dashicons-megaphone', 26 );
} );

// Priority 999 runs last, so the item is there to remove.
add_action( 'admin_menu', function () {
	remove_menu_page( 'noisy-plugin' );
}, 999 );

// Removing the menu does not block the URL, so gate the screen too.
add_action( 'admin_init', function () {
	if ( isset( $_GET['page'] ) && 'noisy-plugin' === $_GET['page'] ) {
		wp_die( esc_html__( 'That screen has been disabled.' ) );
	}
} );

Run it, then look for "Noisy Plugin" in the sidebar: it was registered and then removed.

Common problems and fixes · 4

Why doesn't my admin menu page show up?

Three causes, in the order worth checking.
- The current user lacks the capability you passed. add_menu_page() silently registers nothing the user cannot see, so a typo like manage_option produces no menu and no error. - The callback was hooked too late. Anything added after admin_menu has finished, for instance from admin_init or from inside a template, arrives after the menu is built. - You are on a screen this hook does not run on. The network admin fires network_admin_menu and the user admin fires user_admin_menu; admin_menu covers neither.

Why is remove_menu_page() not removing anything?

Almost always priority. Both the plugin that adds the item and your removal are on the same hook, and the default priority of 10 means yours can run first, removing something that does not exist yet.
`` add_action( 'admin_menu', 'my_removal', 999 ); ``
Also note the slug argument is the menu slug, not the label: for a core item it is the file name, such as edit-comments.php. And removal only hides the item, it does not protect the screen, so check the capability on admin_init too.

What is the difference between admin_menu and admin_init?

admin_menu fires once the admin is loaded and is where the menu is assembled; it is the only correct place to register menu and submenu pages. admin_init fires on every admin request afterwards and is where settings registration, redirects and form handling belong. Registering a menu on admin_init is too late, and running a redirect on admin_menu fires before the screen has been resolved.

Does admin_menu run on the front end or during AJAX?

No to the front end. It runs on admin page loads only, which is why the callback can assume is_admin() is true. AJAX is the trap: admin-ajax.php is an admin request, so admin_menu does fire there, and building a menu on every AJAX call is wasted work. If the callback does anything expensive, guard it with wp_doing_ajax().

Alternatives and related functions

network_admin_menu
When the item belongs in the network admin of a multisite install, where admin_menu never fires.
admin_init
When you are registering settings, handling a form, or redirecting, rather than adding a menu item.
add_menu_page
When you want the function that actually creates the item; this hook is only the moment to call it.
admin_bar_menu
When the item belongs in the front-end toolbar rather than the admin sidebar.

Where this hook fires · 1

  • wp-admin/includes/menu.php:168file scope

Source code

	 * Fires before the administration menu loads in the admin.	 *	 * @since 1.5.0	 *	 * @param string $context Empty context.	 */	do_action( 'admin_menu', '' );} /* * Remove menus that have no accessible submenus and require privileges * that the user does not have. Run re-parent loop again. */

Changelog

Introduced in 1.5.0. Unchanged from 6.7.7 through 7.1.0.

  1. 6.7.7
  2. 6.8.8
  3. 6.9.7
  4. 7.0.4
  5. 7.1.0

Signature, return type and hooks compared across 5 parsed releases.

About this page

Parsed data
Generated from the wordpress-develop 7.1.0 tag, and regenerated for each WordPress release so it tracks the code rather than a snapshot of it.
Corrections
Something wrong on this page? Report it and it gets fixed in the next regeneration.