Opens in a new tab
  1. Home
  2. Guides
  3. Backend
Backend guide

How to Add Custom Admin Menu Items in WordPress

Learn how to add custom WordPress admin menu items and submenus using native APIs, control access with capabilities, create secure admin pages, load assets only where needed and design maintainable plugin navigation.

  • Updated September 21, 2026
  • 25 min read
  • WordPress guide

Adding custom admin menu items in WordPress lets you create dedicated navigation entries for plugin settings, internal tools, reports, client workflows, custom dashboards and almost any other functionality that belongs inside the WordPress administration area.

WordPress provides native APIs for adding both top-level menu items and submenu pages. The two most important functions are add_menu_page() and add_submenu_page(), normally registered through the admin_menu action.

A minimal custom menu can be created with only a few lines of PHP:

add_action( 'admin_menu', 'myplugin_register_admin_menu' );

function myplugin_register_admin_menu() {
    add_menu_page(
        'My Plugin',
        'My Plugin',
        'manage_options',
        'my-plugin',
        'myplugin_render_admin_page',
        'dashicons-admin-generic',
        60
    );
}

function myplugin_render_admin_page() {
    echo '<div class="wrap">';
    echo '<h1>My Plugin</h1>';
    echo '<p>Welcome to the custom admin page.</p>';
    echo '</div>';
}

That example works, but production admin interfaces require more thought. Menu placement, capabilities, page slugs, submenu architecture, icons, callbacks, asset loading, forms, nonces and direct URL access all affect whether the implementation is maintainable and secure.

This guide explains the complete process, from creating a single menu item to building structured WordPress admin navigation for larger plugins and custom client systems.

How the WordPress admin menu works

The left-hand WordPress administration menu is generated dynamically.

WordPress core registers entries such as:

Dashboard
Posts
Media
Pages
Comments
Appearance
Plugins
Users
Tools
Settings

Plugins and themes can then register additional menu entries through the same administration APIs.

The official WordPress admin_menu documentation describes the admin_menu action as the appropriate hook for adding extra menus and submenus to the administration structure.

A typical registration flow looks like this:

WordPress loads admin
↓
plugins initialize
↓
admin_menu fires
↓
plugin registers menu pages
↓
WordPress builds admin navigation
↓
current user sees permitted items

The admin_menu hook

Custom administration menu items should normally be registered through:

add_action( 'admin_menu', 'myplugin_register_admin_menu' );

Then your callback registers the required pages:

function myplugin_register_admin_menu() {

    add_menu_page(
        'My Plugin',
        'My Plugin',
        'manage_options',
        'my-plugin',
        'myplugin_render_admin_page'
    );
}

Do not call add_menu_page() randomly while the plugin file is loading. Registering it on the appropriate hook keeps the code aligned with the WordPress administration lifecycle.

Adding a top-level admin menu item

The native function for creating a new top-level item is add_menu_page().

The official add_menu_page() reference documents the function signature:

add_menu_page(
    $page_title,
    $menu_title,
    $capability,
    $menu_slug,
    $callback,
    $icon_url,
    $position
);

Each argument controls a different part of the resulting admin interface.

page_title

The first argument controls the page title displayed in the browser and associated with the administration screen.

'My Plugin Settings'

For example:

add_menu_page(
    'My Plugin Settings',
    'My Plugin',
    'manage_options',
    'my-plugin',
    'myplugin_render_admin_page'
);

The page title can be more descriptive than the menu label because it is not constrained by the narrow WordPress sidebar.

menu_title

The second argument is the label users see in the left-hand admin menu:

'My Plugin'

Keep menu labels short and recognizable.

Something like:

Client Reports

is usually more useful than:

Access the Complete Client Reporting Management Interface

The sidebar has limited horizontal space and was not designed to accommodate small novels.

capability

The capability determines which users are allowed to access the menu page.

For example:

'manage_options'

is commonly used for site configuration screens intended for administrators.

However, capabilities should be selected according to what the page actually allows users to do.

WordPress permissions are capability-based rather than simply role-name-based. The distinction is explained in WordPress User Roles and Capabilities, Explained.

The official WordPress Roles and Capabilities documentation provides the broader permission model used by WordPress.

menu_slug

The menu slug uniquely identifies the administration page:

'my-plugin'

The resulting URL will usually resemble:

https://example.com/wp-admin/admin.php?page=my-plugin

Choose stable, unique slugs.

Plugin-prefixed values are preferable:

theonewp-tools
theonewp-settings
myplugin-reports

rather than extremely generic values such as:

settings
tools
dashboard

Unique slugs reduce the risk of collisions with WordPress core or another plugin.

callback

The callback generates the content displayed when the admin page is opened.

function myplugin_render_admin_page() {

    ?>

    <div class="wrap">

        <h1>
            <?php esc_html_e( 'My Plugin', 'myplugin' ); ?>
        </h1>

        <p>
            <?php esc_html_e(
                'Configure your plugin settings here.',
                'myplugin'
            ); ?>
        </p>

    </div>

    <?php
}

Using WordPress’s standard wrap container helps the page behave more consistently with the surrounding admin interface.

If the screen itself becomes substantial, the broader architecture is covered in How to Add a Custom Admin Page in WordPress.

icon_url

The sixth argument controls the icon displayed beside the menu label.

WordPress supports Dashicons directly:

'dashicons-admin-generic'

For example:

add_menu_page(
    'Analytics',
    'Analytics',
    'manage_options',
    'myplugin-analytics',
    'myplugin_render_analytics',
    'dashicons-chart-area',
    60
);

The official Dashicons resource lists the icon font included with WordPress.

Using an SVG icon

Custom SVG icons can also be supplied as data URIs.

A common pattern is:

$icon = 'data:image/svg+xml;base64,' . base64_encode( $svg );

add_menu_page(
    'My Plugin',
    'My Plugin',
    'manage_options',
    'my-plugin',
    'myplugin_render_admin_page',
    $icon,
    60
);

When using custom SVG markup, keep the icon intentionally simple and make sure the source is controlled by your plugin rather than accepting arbitrary SVG input from untrusted users.

position

The final argument controls where the top-level menu is placed.

60

WordPress core uses different numeric positions for its built-in sections.

The exact menu should not depend too heavily on forcing a specific slot because plugins may compete for similar positions and WordPress handles collisions.

Think of position as a placement preference rather than an absolute visual coordinate.

A complete top-level menu example

add_action( 'admin_menu', 'myplugin_register_menu' );

function myplugin_register_menu() {

    add_menu_page(
        __( 'Client Dashboard', 'myplugin' ),
        __( 'Clients', 'myplugin' ),
        'manage_options',
        'myplugin-clients',
        'myplugin_render_clients_page',
        'dashicons-groups',
        58
    );
}

function myplugin_render_clients_page() {

    if ( ! current_user_can( 'manage_options' ) ) {
        wp_die(
            esc_html__(
                'You do not have permission to access this page.',
                'myplugin'
            )
        );
    }

    ?>

    <div class="wrap">

        <h1>
            <?php esc_html_e(
                'Client Dashboard',
                'myplugin'
            ); ?>
        </h1>

        <p>
            <?php esc_html_e(
                'Manage client information from this screen.',
                'myplugin'
            ); ?>
        </p>

    </div>

    <?php
}

Why check the capability again inside the callback?

The capability passed to add_menu_page() controls access to the registered menu page, but sensitive operations should still enforce authorization where those operations actually occur.

A useful rule is:

menu capability
→ controls page access

operation capability check
→ controls the privileged action

The official current_user_can() documentation describes the standard API for checking whether the current user has a capability.

This becomes especially important once a page can save settings, delete records, export data or trigger administrative actions.

Hiding a menu item is not security

This distinction is critical.

Removing a link from the sidebar does not necessarily prevent someone from requesting the underlying admin URL directly.

For example:

Menu item hidden

but user manually requests:

/wp-admin/admin.php?page=myplugin-settings

Real authorization must be based on capabilities and server-side checks.

This difference is explored further in Controlling WordPress Admin Page Visibility by Role and How to Hide WordPress Admin Menu Items by Role.

Adding a submenu item

Not every tool deserves a new top-level menu entry.

WordPress provides add_submenu_page() for adding pages beneath an existing parent.

The official add_submenu_page() documentation defines the function as:

add_submenu_page(
    $parent_slug,
    $page_title,
    $menu_title,
    $capability,
    $menu_slug,
    $callback,
    $position
);

Add a submenu beneath your own custom menu

Suppose your top-level menu uses:

myplugin

You can register additional screens beneath it:

add_action( 'admin_menu', 'myplugin_register_admin_pages' );

function myplugin_register_admin_pages() {

    add_menu_page(
        'My Plugin',
        'My Plugin',
        'manage_options',
        'myplugin',
        'myplugin_render_dashboard',
        'dashicons-admin-generic',
        60
    );

    add_submenu_page(
        'myplugin',
        'Reports',
        'Reports',
        'manage_options',
        'myplugin-reports',
        'myplugin_render_reports'
    );

    add_submenu_page(
        'myplugin',
        'Settings',
        'Settings',
        'manage_options',
        'myplugin-settings',
        'myplugin_render_settings'
    );
}

The resulting navigation becomes:

My Plugin
├── My Plugin
├── Reports
└── Settings

Why does WordPress duplicate the first submenu?

When a top-level menu page is created, WordPress normally creates a corresponding first submenu entry using the same page.

So this:

My Plugin

can become:

My Plugin
├── My Plugin
├── Reports
└── Settings

That behavior is normal.

Renaming the first submenu item

If you want the first submenu to say Dashboard instead of repeating the plugin name, explicitly register a submenu using the same slug as the parent:

add_menu_page(
    'My Plugin',
    'My Plugin',
    'manage_options',
    'myplugin',
    'myplugin_render_dashboard',
    'dashicons-admin-generic',
    60
);

add_submenu_page(
    'myplugin',
    'Dashboard',
    'Dashboard',
    'manage_options',
    'myplugin',
    'myplugin_render_dashboard'
);

The navigation becomes:

My Plugin
├── Dashboard
├── Reports
└── Settings

That is usually clearer for plugins containing several administration screens.

Adding a submenu beneath Settings

You can add pages beneath existing WordPress menus.

For Settings:

add_action( 'admin_menu', 'myplugin_register_settings_page' );

function myplugin_register_settings_page() {

    add_options_page(
        'My Plugin Settings',
        'My Plugin',
        'manage_options',
        'myplugin-settings',
        'myplugin_render_settings_page'
    );
}

add_options_page() is a convenience wrapper for registering a submenu beneath the WordPress Settings menu.

The official add_options_page() reference documents the API.

Adding a submenu beneath Tools

For administrative utilities, Tools may be more appropriate:

add_action( 'admin_menu', 'myplugin_register_tools_page' );

function myplugin_register_tools_page() {

    add_management_page(
        'Data Importer',
        'Data Importer',
        'manage_options',
        'myplugin-importer',
        'myplugin_render_importer'
    );
}

This produces:

Tools
└── Data Importer

Adding a submenu beneath Appearance

Theme-related functionality may belong under Appearance:

add_theme_page(
    'Theme Tools',
    'Theme Tools',
    'edit_theme_options',
    'my-theme-tools',
    'myplugin_render_theme_tools'
);

Notice the capability:

edit_theme_options

That describes the permission required by the functionality more accurately than blindly using manage_options everywhere.

Adding a submenu beneath Users

User-management tools can be placed beneath Users:

add_users_page(
    'User Audit',
    'User Audit',
    'list_users',
    'myplugin-user-audit',
    'myplugin_render_user_audit'
);

If your custom screen deals with user permissions, it is worth understanding the existing access model before extending it. See How to Audit User Roles on a WordPress Site.

Choosing between a top-level menu and a submenu

Create a top-level menu when the feature represents a substantial independent area of the WordPress administration interface.

Examples include:

CRM
Bookings
Analytics
SEO Suite
Security
Client Portal
Ecommerce Management

Use a submenu when the feature is a smaller configuration or utility screen that naturally belongs beneath an existing section.

Examples:

Settings → Analytics Settings
Tools → Import Products
Users → Access Audit
Appearance → Theme Options

Adding a top-level menu for every minor plugin setting quickly produces an administration sidebar that requires its own archaeology department.

Use capabilities, not role names

A common mistake is to write logic such as:

if ( in_array( 'administrator', $user->roles, true ) ) {
    // Show menu.
}

This is usually less flexible than checking the capability required by the operation:

if ( current_user_can( 'manage_options' ) ) {
    // User is authorized.
}

Capabilities allow custom roles and modified role structures to participate correctly.

For a deeper explanation, see WordPress User Roles and Capabilities, Explained.

Use a custom capability for plugin-specific access

For larger plugins, a custom capability can provide better control than relying entirely on a broad core capability.

For example:

manage_myplugin

Your menu can then require:

add_menu_page(
    'My Plugin',
    'My Plugin',
    'manage_myplugin',
    'myplugin',
    'myplugin_render_admin_page'
);

The custom capability can be assigned to whichever roles should manage the feature.

This is particularly useful when administrators should have access but some custom managerial role should also be allowed in.

Separate menu visibility from feature permissions

Consider a plugin containing:

Dashboard
Reports
Settings
Danger Zone

These screens do not necessarily need the same capability.

You could define:

Dashboard
→ read_myplugin_reports

Reports
→ read_myplugin_reports

Settings
→ manage_myplugin

Danger Zone
→ manage_myplugin_dangerous_actions

This gives you more granular control than making every screen administrator-only.

The broader pattern is covered in Restricting WordPress Features by User Role.

Custom admin menu structure example

A larger plugin could register an architecture like:

My Plugin
├── Dashboard
├── Reports
├── Integrations
├── Users
└── Settings

The PHP might look like:

add_action( 'admin_menu', 'myplugin_register_admin_menu' );

function myplugin_register_admin_menu() {

    $capability = 'manage_options';

    add_menu_page(
        'My Plugin',
        'My Plugin',
        $capability,
        'myplugin',
        'myplugin_render_dashboard',
        'dashicons-admin-generic',
        58
    );

    add_submenu_page(
        'myplugin',
        'Dashboard',
        'Dashboard',
        $capability,
        'myplugin',
        'myplugin_render_dashboard'
    );

    add_submenu_page(
        'myplugin',
        'Reports',
        'Reports',
        $capability,
        'myplugin-reports',
        'myplugin_render_reports'
    );

    add_submenu_page(
        'myplugin',
        'Integrations',
        'Integrations',
        $capability,
        'myplugin-integrations',
        'myplugin_render_integrations'
    );

    add_submenu_page(
        'myplugin',
        'Users',
        'Users',
        $capability,
        'myplugin-users',
        'myplugin_render_users'
    );

    add_submenu_page(
        'myplugin',
        'Settings',
        'Settings',
        $capability,
        'myplugin-settings',
        'myplugin_render_settings'
    );
}

Return values from add_menu_page()

add_menu_page() returns a hook suffix identifying the registered screen.

You can store it:

$hook_suffix = add_menu_page(
    'My Plugin',
    'My Plugin',
    'manage_options',
    'myplugin',
    'myplugin_render_dashboard'
);

This value is useful when loading assets only on the screen that needs them.

Do not load admin CSS and JavaScript everywhere

A custom administration page may require:

admin.css
admin.js
charts.js
select component
code editor
media tools

Do not enqueue all of them across every WordPress admin screen.

The admin_enqueue_scripts hook provides the current admin page hook suffix.

The official admin_enqueue_scripts documentation explains this mechanism.

For example:

add_action(
    'admin_enqueue_scripts',
    'myplugin_admin_assets'
);

function myplugin_admin_assets( $hook_suffix ) {

    if ( 'toplevel_page_myplugin' !== $hook_suffix ) {
        return;
    }

    wp_enqueue_style(
        'myplugin-admin',
        plugin_dir_url( __FILE__ ) . 'assets/admin.css',
        array(),
        '1.0.0'
    );

    wp_enqueue_script(
        'myplugin-admin',
        plugin_dir_url( __FILE__ ) . 'assets/admin.js',
        array(),
        '1.0.0',
        true
    );
}

Why scoped asset loading matters

Loading plugin assets globally can:

  • slow down unrelated admin pages;
  • cause CSS collisions;
  • cause JavaScript conflicts;
  • load large libraries unnecessarily;
  • interfere with third-party plugin interfaces.

A plugin administration stylesheet should generally affect the plugin’s own screens, not quietly redesign WooCommerce, the Block Editor and the Users table because a selector was feeling ambitious.

Using get_current_screen()

For more complex targeting, WordPress provides get_current_screen().

The official get_current_screen() reference documents the current administration screen object.

For example:

$screen = get_current_screen();

if (
    ! $screen ||
    'toplevel_page_myplugin' !== $screen->id
) {
    return;
}

This can be useful when a plugin has several admin screens requiring different assets or behavior.

Adding forms to custom menu pages

A custom menu item often leads to a settings form.

Once a page modifies server-side data, you need more than a menu callback.

A secure form should consider:

capability check
+
nonce
+
input validation
+
sanitization
+
safe persistence
+
escaped output

Use nonces for state-changing actions

WordPress nonces help protect state-changing requests from cross-site request forgery.

The official WordPress Nonces documentation explains their purpose and limitations.

A simple form might include:

<form method="post">

    <?php
    wp_nonce_field(
        'myplugin_save_settings',
        'myplugin_nonce'
    );
    ?>

    <input
        type="text"
        name="myplugin_name"
        value=""
    >

    <?php submit_button(); ?>

</form>

Then validate it during processing:

if (
    ! isset( $_POST['myplugin_nonce'] ) ||
    ! wp_verify_nonce(
        sanitize_text_field(
            wp_unslash( $_POST['myplugin_nonce'] )
        ),
        'myplugin_save_settings'
    )
) {
    wp_die( 'Invalid request.' );
}

A nonce is not authorization

Passing a nonce check does not prove that a user is allowed to perform the operation.

Use both:

current_user_can()
+
nonce verification

For example:

if ( ! current_user_can( 'manage_options' ) ) {
    wp_die( 'Unauthorized.' );
}

check_admin_referer(
    'myplugin_save_settings',
    'myplugin_nonce'
);

Sanitize submitted values

Input received from a form should be treated as untrusted.

WordPress provides dedicated sanitization APIs documented in the official Sanitizing Data guide.

For a text field:

$name = isset( $_POST['myplugin_name'] )
    ? sanitize_text_field(
        wp_unslash( $_POST['myplugin_name'] )
    )
    : '';

The correct sanitization function depends on the type of value being accepted.

Escape output

Data should also be escaped according to its output context.

The official WordPress Escaping Data documentation covers the main escaping functions.

For example:

echo esc_html( $name );

or:

<input
    type="text"
    value="<?php echo esc_attr( $name ); ?>"
>

Do not trust query-string values

Custom admin pages commonly use URLs such as:

/wp-admin/admin.php?page=myplugin&item=42

The item parameter remains user-controlled input.

For an integer ID:

$item_id = isset( $_GET['item'] )
    ? absint( $_GET['item'] )
    : 0;

Then verify that the current user is allowed to interact with the referenced object.

Menu visibility for different user roles

Suppose you want editors to see a reporting page but only administrators to see settings.

You can use different capabilities:

add_submenu_page(
    'myplugin',
    'Reports',
    'Reports',
    'edit_others_posts',
    'myplugin-reports',
    'myplugin_render_reports'
);

add_submenu_page(
    'myplugin',
    'Settings',
    'Settings',
    'manage_options',
    'myplugin-settings',
    'myplugin_render_settings'
);

Users see only the menu items for which they satisfy the required capability.

For more advanced role-specific navigation, see How to Hide WordPress Admin Menu Items by Role.

Removing existing menu items

WordPress provides remove_menu_page() for removing top-level menu entries from the visible admin menu.

The official remove_menu_page() documentation covers the function.

For example:

add_action(
    'admin_menu',
    'myplugin_customize_menu',
    999
);

function myplugin_customize_menu() {

    remove_menu_page( 'edit-comments.php' );
}

The high priority allows the callback to run after many other menu items have already been registered.

Removing submenu items

WordPress also provides remove_submenu_page().

The official remove_submenu_page() reference documents its arguments.

For example:

remove_submenu_page(
    'options-general.php',
    'options-permalink.php'
);

Again, removing navigation is not equivalent to removing permission.

Reorganizing the admin menu without custom PHP

When the goal is primarily to reorganize existing menu items rather than create entirely new application screens, custom code may be unnecessary.

The TheOneWP Admin Menu Organizer provides a dedicated interface for reorganizing the WordPress administration menu.

This is useful when the objective is operational:

reorder items
rename items
reduce clutter
adapt navigation for a workflow

rather than developing a new plugin screen from scratch.

Admin menu organization and access control are different

A menu organizer changes navigation.

An access-control system changes permissions.

Those are separate concerns.

If you need to manage access rather than merely rearrange what users see, the TheOneWP Access Manager addresses the permission side of the administration experience.

The distinction can be summarized as:

Admin Menu Organizer
→ how navigation is presented

Access Manager
→ what users are allowed to access

Creating menu links to external URLs

add_menu_page() is designed around WordPress administration pages.

If you merely want to send users to an external resource, consider whether the WordPress sidebar is genuinely the correct place for that link.

For plugin documentation or support, contextual links within your own plugin screen may provide a cleaner experience.

Admin navigation should primarily help users navigate administration functionality rather than becoming a collection of promotional links.

Creating links to existing WordPress admin screens

Sometimes you do not need a new screen at all.

You may simply need to link users to an existing administration page.

WordPress provides admin_url() for generating administration URLs.

The official admin_url() reference documents the function.

For example:

$url = admin_url(
    'edit.php?post_type=product'
);

Avoid hardcoding the entire domain:

https://example.com/wp-admin/...

because WordPress may be installed or configured differently across environments.

Use stable slugs

Once users, documentation, bookmarks or integrations depend on:

admin.php?page=myplugin-reports

changing the slug can break those references.

Treat menu slugs as stable identifiers rather than labels that should be renamed whenever the marketing terminology changes.

You can change:

menu title:
Reports → Analytics

without necessarily changing:

menu slug:
myplugin-reports

Prefix your functions too

WordPress runs many plugins in the same PHP environment.

A generic function such as:

function render_settings() {}

can collide with another plugin defining the same function.

Prefer:

function myplugin_render_settings() {}

or use a class or namespace for larger codebases.

Object-oriented menu registration

A larger plugin might encapsulate admin navigation in a class:

class MyPlugin_Admin_Menu {

    public function register() {

        add_action(
            'admin_menu',
            array( $this, 'add_pages' )
        );
    }

    public function add_pages() {

        add_menu_page(
            'My Plugin',
            'My Plugin',
            'manage_options',
            'myplugin',
            array( $this, 'render_dashboard' ),
            'dashicons-admin-generic',
            60
        );
    }

    public function render_dashboard() {

        if ( ! current_user_can( 'manage_options' ) ) {
            wp_die( 'Unauthorized.' );
        }

        echo '<div class="wrap">';
        echo '<h1>My Plugin</h1>';
        echo '</div>';
    }
}

$admin_menu = new MyPlugin_Admin_Menu();
$admin_menu->register();

This structure becomes easier to maintain as the plugin gains additional screens and services.

Using namespaces

Modern plugins may use PHP namespaces:

namespace MyPlugin\Admin;

class Menu {

    public function register(): void {

        add_action(
            'admin_menu',
            array( $this, 'register_pages' )
        );
    }

    public function register_pages(): void {

        add_menu_page(
            'My Plugin',
            'My Plugin',
            'manage_options',
            'myplugin',
            array( $this, 'render' )
        );
    }

    public function render(): void {

        echo '<div class="wrap">';
        echo '<h1>My Plugin</h1>';
        echo '</div>';
    }
}

The menu API itself does not require object-oriented code. Architecture becomes relevant when the surrounding plugin is large enough that global callbacks become difficult to manage.

Internationalize menu labels

Plugin-facing text should be translatable.

Instead of:

'Settings'

use:

__( 'Settings', 'myplugin' )

For example:

add_menu_page(
    __( 'My Plugin Settings', 'myplugin' ),
    __( 'My Plugin', 'myplugin' ),
    'manage_options',
    'myplugin',
    'myplugin_render_page'
);

The official WordPress plugin internationalization guide explains the broader localization workflow.

Accessibility matters in custom admin pages

Registering the menu is only the beginning.

The page behind it should remain usable with:

  • keyboard navigation;
  • visible focus states;
  • proper labels;
  • semantic headings;
  • accessible form controls;
  • clear validation messages;
  • sufficient contrast.

WordPress publishes dedicated Accessibility Coding Standards for administration and frontend development.

Use WordPress admin conventions where they help

Custom admin screens do not have to look identical to every core page, but familiar conventions reduce the amount users need to relearn.

Common patterns include:

.wrap
standard heading hierarchy
WordPress notices
standard buttons
familiar form controls
tables where appropriate

A completely custom application-style interface can be justified for complex functionality, but it should be a deliberate design decision.

Adding admin notices to your page

When an action succeeds or fails, users need feedback.

WordPress admin notices can communicate status such as:

Settings saved.
Import completed.
Connection failed.
Permission denied.

The admin_notices hook is one of the native mechanisms available for administration notices.

Avoid processing destructive actions directly in rendering callbacks

A page-render callback should primarily render the screen.

For significant state-changing operations, separate the request handling from presentation.

For example:

admin page
↓
form submission
↓
dedicated handler
↓
capability check
↓
nonce verification
↓
validation
↓
operation
↓
redirect
↓
success notice

This architecture is easier to reason about and reduces accidental resubmission.

Use admin-post.php for dedicated actions

WordPress provides admin-post.php as a convenient endpoint for authenticated administrative form actions.

A form can submit to:

<form
    method="post"
    action="<?php echo esc_url(
        admin_url( 'admin-post.php' )
    ); ?>"
>

    <input
        type="hidden"
        name="action"
        value="myplugin_export"
    >

    <?php
    wp_nonce_field(
        'myplugin_export',
        'myplugin_nonce'
    );
    ?>

    <button type="submit">
        Export
    </button>

</form>

Then register the handler:

add_action(
    'admin_post_myplugin_export',
    'myplugin_handle_export'
);

function myplugin_handle_export() {

    if ( ! current_user_can( 'manage_options' ) ) {
        wp_die( 'Unauthorized.' );
    }

    check_admin_referer(
        'myplugin_export',
        'myplugin_nonce'
    );

    // Perform export.
}

Redirect after processing

After completing a state-changing action, redirect back to the administration page:

$url = add_query_arg(
    array(
        'page'    => 'myplugin',
        'updated' => '1',
    ),
    admin_url( 'admin.php' )
);

wp_safe_redirect( $url );
exit;

The official wp_safe_redirect() documentation covers safe local redirects.

Admin menu items for custom post types

Custom post types automatically create administration navigation when configured with the appropriate show_ui and show_in_menu arguments.

You do not always need to manually create a menu item.

For example:

register_post_type(
    'portfolio',
    array(
        'label'        => 'Portfolio',
        'public'       => true,
        'show_ui'      => true,
        'show_in_menu' => true,
    )
);

The official register_post_type() reference documents these arguments.

Place a custom post type beneath another menu

The show_in_menu argument can also associate a custom post type with an existing administration menu structure.

This can help keep related content together instead of creating another top-level item.

For example, a plugin might conceptually organize:

Events
├── All Events
├── Add New
├── Venues
└── Settings

rather than scattering those screens across unrelated parts of the sidebar.

Menu design for client websites

Custom admin menus are especially useful on client-managed WordPress sites.

A client may only need:

Dashboard
Pages
News
Products
Orders
Media
SEO

while the development team still needs access to additional configuration tools.

The goal should not be to hide functionality arbitrarily. It should be to create an administration interface that reflects actual responsibilities.

For role-based interface design, combine the principles from How to Hide WordPress Admin Menu Items by Role with proper capability enforcement.

Do not use menu customization to fake permissions

Suppose a user should not be able to change plugin settings.

This is insufficient:

remove menu item
↓
assume user cannot access settings

The correct model is:

remove or hide menu item
+
restrict underlying page
+
restrict save operation
+
restrict related AJAX/REST actions

The underlying principle is covered in Controlling WordPress Admin Page Visibility by Role.

Audit custom roles before assigning menu capabilities

Sites often accumulate roles created by:

  • membership plugins;
  • ecommerce plugins;
  • LMS systems;
  • agencies;
  • old custom code;
  • previous access-management plugins.

Before granting access to a new administration tool, verify what those roles already contain.

The process is covered in How to Audit User Roles on a WordPress Site.

Common mistake: registering menus too early

This is incorrect architecture:

// Plugin file executes.

add_menu_page(
    ...
);

Use the appropriate hook instead:

add_action(
    'admin_menu',
    'myplugin_register_menu'
);

Hook timing keeps the implementation predictable and compatible with WordPress’s admin lifecycle.

Common mistake: using a role as the capability

A developer may try:

add_menu_page(
    'My Plugin',
    'My Plugin',
    'administrator',
    'myplugin',
    'myplugin_render_page'
);

The third argument is intended to represent a capability required to access the page.

Use something appropriate such as:

manage_options

or a custom capability:

manage_myplugin

Common mistake: generic menu slugs

A slug such as:

settings

is unnecessarily generic.

Prefer:

myplugin-settings

Namespacing identifiers reduces collisions and makes URLs easier to recognize while debugging.

Common mistake: loading huge libraries on every admin page

If your custom dashboard uses a charting library, do not load it on:

Posts
Media
Users
WooCommerce
plugin settings
Block Editor
every other admin page

Load it only where required.

Common mistake: relying only on the menu capability

Do not assume:

menu required manage_options
therefore every request is secure

Any separate AJAX endpoint, REST endpoint, form handler or custom action must perform its own authorization checks.

Common mistake: putting every feature in the top-level menu

Before creating:

Analytics
Reports
Import
Export
Settings
Logs
Tools
Connections
Diagnostics

as eight separate top-level menu entries, consider:

My Plugin
├── Dashboard
├── Analytics
├── Reports
├── Import / Export
├── Connections
├── Logs
├── Diagnostics
└── Settings

A coherent information architecture matters just as much in wp-admin as it does on the frontend.

Reordering an existing WordPress admin menu

If you do not need to create new functionality and only want a cleaner menu structure, editing PHP may be unnecessary.

TheOneWP Admin Menu Organizer is designed for reorganizing the existing administration menu without building a custom menu-management layer manually.

This can be particularly useful for client websites where the functionality already exists but the default navigation no longer reflects the user’s workflow.

Restricting menu access with TheOneWP

When menu organization becomes an access-management problem, use permissions rather than visual hiding alone.

TheOneWP Access Manager provides a dedicated access-control layer, while the underlying WordPress principles remain the same: authorization should ultimately reflect capabilities and actual server-side access.

Testing a custom admin menu

Do not test only while logged in as the primary administrator.

Test at least:

Administrator
Editor
Author
custom roles used by the site

For each relevant role, verify:

  • whether the menu appears;
  • whether the submenu appears;
  • whether the direct URL works or is correctly denied;
  • whether forms can be submitted;
  • whether AJAX actions are authorized;
  • whether REST actions are authorized;
  • whether notices display correctly;
  • whether unrelated admin screens remain unaffected.

Test direct URLs

If a user cannot see:

Settings

in the menu, manually test:

/wp-admin/admin.php?page=myplugin-settings

The expected result depends on the user’s capability.

If the user is unauthorized, the request should not expose the restricted interface merely because they know its URL.

Test plugin activation and deactivation

After developing a custom menu system, test:

activate plugin
↓
menu appears

deactivate plugin
↓
menu disappears

reactivate plugin
↓
menu returns correctly

Also verify that no stale callbacks, cached assets or persistent role changes create unexpected behavior.

Test with other plugins installed

Menu position collisions, CSS conflicts and generic slugs may not appear on an empty development installation.

Test in an environment closer to the production stack.

This is particularly important when using:

generic CSS selectors
generic JavaScript globals
generic menu slugs
common function names
fixed menu positions

Performance considerations

Registering a menu item itself is inexpensive.

The expensive part is often everything attached to the screen.

A custom page might trigger:

large database queries
remote API requests
statistics aggregation
filesystem scans
JavaScript applications
large data tables

Do not perform those operations simply because admin_menu fired.

Register the page during admin_menu, then perform expensive work only when the relevant screen or action actually requires it.

Do not make remote API requests during menu registration

This is poor architecture:

admin_menu
↓
remote API request
↓
wait for response
↓
register menu

Menu registration should remain lightweight.

Remote data should be loaded when the relevant page requires it, ideally with appropriate caching and failure handling.

A practical plugin architecture

A maintainable plugin may separate responsibilities like this:

Plugin
│
├── Admin
│   ├── Menu.php
│   ├── Assets.php
│   ├── Notices.php
│   └── Pages
│       ├── Dashboard.php
│       ├── Reports.php
│       └── Settings.php
│
├── Services
│   ├── Reports.php
│   └── Settings.php
│
└── Security
    └── Capabilities.php

The menu layer then does little more than register navigation and connect screens to their controllers or renderers.

When to use custom code

Custom PHP is appropriate when you are:

  • developing a plugin;
  • creating a custom administration application;
  • building client-specific workflows;
  • connecting menu pages to custom business logic;
  • creating new screens rather than reorganizing existing ones;
  • requiring fine-grained integration with WordPress APIs.

When not to use custom code

If the requirement is simply:

move Posts lower
rename a menu item
hide unnecessary entries
create a cleaner client menu

a dedicated administration-menu tool may be more maintainable.

That is the use case addressed by Admin Menu Organizer.

Production checklist

  • Register menu pages on admin_menu.
  • Use add_menu_page() for genuine top-level sections.
  • Use add_submenu_page() for related child screens.
  • Use WordPress convenience functions for Settings, Tools, Appearance and Users where appropriate.
  • Use unique prefixed menu slugs.
  • Choose capabilities based on the operation being exposed.
  • Prefer capabilities over hardcoded role names.
  • Use custom capabilities when a plugin requires granular access.
  • Check permissions again for privileged operations.
  • Remember that hiding navigation is not authorization.
  • Use nonces for state-changing requests.
  • Sanitize input.
  • Escape output.
  • Load CSS and JavaScript only on relevant admin screens.
  • Use stable menu slugs.
  • Internationalize visible labels.
  • Keep administration pages accessible.
  • Avoid unnecessary top-level menu entries.
  • Test direct URLs.
  • Test multiple user roles.
  • Test custom roles.
  • Test activation and deactivation.
  • Test with the production plugin stack.
  • Keep expensive work out of menu registration.
  • Separate navigation, permissions and business logic.

Related guides

Related TheOneWP features

Admin Menu Organizer provides a visual way to reorganize the WordPress administration menu when the objective is to simplify or restructure existing navigation rather than create new plugin screens manually.

Access Manager complements menu organization by addressing access control rather than presentation alone.

Final recommendation

WordPress already provides a complete native API for building custom administration navigation. Use add_menu_page() for major independent sections and add_submenu_page() for screens that belong within an existing hierarchy.

The most important architectural decision is not the icon, label or menu position. It is the capability associated with the page. Navigation should reflect what users are permitted to do, while the underlying operations must enforce those permissions independently.

Keep menu registration lightweight, use unique slugs, scope admin assets to the screens that need them and separate navigation from business logic. For forms and state-changing actions, combine capability checks with nonces, sanitization, validation and escaped output.

For a plugin with several screens, design the admin menu as an information architecture rather than a collection of unrelated links:

Plugin
├── Dashboard
├── Main workflows
├── Reports
├── Integrations
└── Settings

And when the requirement is only to reorganize existing WordPress navigation rather than develop new functionality, TheOneWP Admin Menu Organizer can handle that layer without introducing another custom menu implementation.

Simplify your WordPress stack

A modular WordPress toolkit. 104 focused tools.

Ultimately, you can build cleaner workflows, maintain fewer plugins and enable only the features each website actually needs.