Inside WordPress: anatomy of the wp-admin dashboard and templates

Inside WordPress: anatomy of the wp-admin dashboard and templates

Last verified: October 16, 2026
16 min read
Guide
500+ WP projects
Full-stack developer

#What the WordPress administration panel consists of

The WordPress administration panel looks like one application, but technically it is a set of plain PHP files in the wp-admin directory, tied together by one bootstrap file and a few dozen actions. Developers who know the order in which those actions run write plugins that load scripts only where they are needed and do not break the screens of other plugins.

This article covers the wp-admin directory, the path of a single request through the panel, how screens are named ($hook_suffix, WP_Screen) and how the panel differs from the theme template hierarchy. File names and action order come from the WordPress 7.1.3 code (the current stable release at the time of checking) in the WordPress/WordPress mirror on GitHub.

#What is the wp-admin directory structure in WordPress

The wp-admin directory holds the back-end files of the site. The top level has one file per screen: edit.php is the post list, post.php and post-new.php handle editing, upload.php is the Media Library, options-general.php, options-writing.php and the other options-*.php files are the Settings subpages, and site-health.php is Site Health. The address in the browser bar therefore tells you which file is running. Next to them sit the subdirectories network/ (the network admin in multisite), user/ (the user admin in multisite) and maint/ (database repair).

#Core files in the wp-admin directory

admin.php - The bootstrap file of the panel. Every screen starts with require_once __DIR__ . '/admin.php';. This is where:

  • the WP_ADMIN constant is defined and wp-load.php is loaded
  • the functions from wp-admin/includes/admin.php are included
  • auth_redirect() sends logged-out visitors to the login screen
  • the menu is built and the current screen is set

index.php - The Dashboard, the landing page of the panel. It shows:

  • dashboard widgets
  • quick actions
  • WordPress news

menu.php - The definition of the panel menu structure:

  • registration of menu items
  • the capabilities required for each section
  • menu icons and labels

#Key subdirectories in wp-admin

includes/ - Holds all the library files:

  • class-wp-screen.php - the class that manages panel screens
  • template.php - helper functions for templates
  • media.php - media handling
  • ajax-actions.php - AJAX handlers

css/ - The panel stylesheets:

  • wp-admin.css - the aggregate stylesheet that imports the others
  • common.css - shared styles (the .wrap container, .notice notices)
  • colors/ - color schemes
  • forms.css - form styles

Each stylesheet ships in four variants: the source file, a minified one (.min.css) and two versions for right-to-left languages (-rtl.css, -rtl.min.css). The same applies to the scripts in js/: common.min.js sits next to common.js. In production WordPress serves the .min versions, and the SCRIPT_DEBUG constant switches it to the source files, as described below.

js/ - JavaScript files:

  • common.js - shared behavior
  • post.js - the post editing screen
  • media-upload.js - media upload

#What WordPress does when you open a wp-admin address

Take the request wp-admin/admin.php?page=my-page, a screen added by a plugin. In version 7.1.3 admin.php goes through these steps in order:

  1. It defines WP_ADMIN and includes wp-load.php, and with it wp-config.php, the plugins and the theme. From this point is_admin() returns true.
  2. It includes wp-admin/includes/admin.php, the whole panel API, and calls auth_redirect().
  3. It includes wp-admin/menu.php. That file builds the $menu and $submenu arrays with the core items and finally includes wp-admin/includes/menu.php, which fires the admin_menu action. This is where plugins call add_menu_page().
  4. It fires the admin_init action.
  5. From the page parameter it computes the page hook name (get_plugin_page_hook()) and stores it in the global $hook_suffix variable.
  6. It calls set_current_screen(), which creates the WP_Screen object for this screen.
  7. It fires the load-{$page_hook} action, before any HTML has been sent.
  8. It includes admin-header.php, unless the address contains the noheader parameter.
  9. It fires the action named $page_hook. That is where add_menu_page() attached the plugin callback earlier.
  10. It includes admin-footer.php and ends with exit.

Two consequences of this order break plugins in practice. First, admin_menu runs before admin_init, not after it, so menu registration code cannot assume that something set up in admin_init already exists. Second, get_current_screen() returns null until step 6, because the function returns the global object only when it is an instance of WP_Screen. Calling it in admin_init gives null, while in load-{$page_hook} it already returns a working object.

The load-{$page_hook} action is the best place to process a form that redirects after saving. HTTP headers have not been sent yet, so wp_safe_redirect() works, and the code runs on that one screen only. A comment in admin.php gives an example name: for a plugin page added under Settings the hook is called load-settings_page_pluginbasename.

#What runs in admin-header.php and admin-footer.php

The panel header is also a sequence of actions, and their order decides where styles and scripts should be attached. In admin-header.php WordPress fires, in order:

  • admin_enqueue_scripts with the $hook_suffix argument, the place for wp_enqueue_script() and wp_enqueue_style(),
  • admin_print_styles-{$hook_suffix} and admin_print_styles,
  • admin_print_scripts-{$hook_suffix} and admin_print_scripts,
  • admin_head-{$hook_suffix} and admin_head,
  • then it includes menu-header.php, which draws the side menu, and fires in_admin_header,
  • finally admin_notices (or network_admin_notices and user_admin_notices in multisite) and all_admin_notices.

In admin-footer.php the order is similar: in_admin_footer, admin_footer, admin_print_footer_scripts-{$hook_suffix}, admin_print_footer_scripts and admin_footer-{$hook_suffix}. The suffixed versions run on one screen only, so they replace an if condition inside the callback. The documentation of the admin_enqueue_scripts hook calls it the proper hook for enqueuing scripts and styles meant for the admin (despite the name it handles both) and recommends using $hook_suffix to load them only on the screens that use them.

Core screens such as options-general.php do the same without a page parameter: they start with require_once __DIR__ . '/admin.php';, prepare their data, include admin-header.php themselves, print the form and include admin-footer.php.

#How the administration template system works

WordPress does not use a traditional template system in the administration panel like the one on the front end. Instead it relies on a structure built from callback functions and hooks.

#Structure of an administration screen

Every panel screen consists of three main parts. The sketch below shows the layout of a core screen, which includes the header and footer itself. A plugin page callback does not do this, because admin.php includes the header and footer for it (steps 8 and 10 above). admin.php includes the header with require_once, so a second require_once in the callback does nothing, but a plain require or include prints the panel header a second time.

// Example structure of a core screen, e.g. wp-admin/options-general.php
require_once __DIR__ . '/admin.php';

// 1. Header
require_once ABSPATH . 'wp-admin/admin-header.php';

// 2. Page content
?>
<div class="wrap">
    <h1><?php echo esc_html(get_admin_page_title()); ?></h1>
    <!-- The actual content goes here -->
</div>
<?php

// 3. Footer
require_once ABSPATH . 'wp-admin/admin-footer.php';

#Callback functions for administration pages

Every panel page has a callback function assigned to it that renders its content:

// Register a panel page
add_action('admin_menu', function() {
    add_menu_page(
        'My Page',               // Page title
        'My Page',               // Menu label
        'manage_options',        // Capability
        'my-page',               // Slug
        'my_page_callback',      // Callback function
        'dashicons-admin-generic',
        30
    );
});

// Callback function that renders the content
function my_page_callback() {
    ?>
    <div class="wrap">
        <h1><?php echo esc_html(get_admin_page_title()); ?></h1>
        <form method="post" action="options.php">
            <?php
            settings_fields('my_options_group');
            do_settings_sections('my-page');
            submit_button('Save Changes');
            ?>
        </form>
    </div>
    <?php
}

#How WordPress names the hook_suffix of plugin pages

The value returned by add_menu_page() and add_submenu_page() is the $hook_suffix of the new screen. The same string goes to admin_enqueue_scripts, to the load-* hooks and to WP_Screen, so it pays to know where it comes from. It is computed by get_plugin_page_hookname() in wp-admin/includes/plugin.php following the pattern {type}_page_{slug}:

  • a top-level page added with add_menu_page() gets the type toplevel, so toplevel_page_my-page,
  • a subpage under a core menu gets a type derived from the parent file name, for example settings_page_my-page under Settings,
  • a subpage under another plugin’s menu gets its type from the $admin_page_hooks array, where add_menu_page() stores sanitize_title( $menu_title ).

The last point is a trap on translated sites. If the parent menu title goes through __(), then changing the site language also changes the suffix of all its subpages. A menu titled __( 'Reports' ) gives its subpage the suffix reports_page_my-reports, but once the plugin’s translation turns the title into “Raporty” on a Polish panel, the suffix becomes raporty_page_my-reports. Code that compares $hook with the first value hard-coded during development stops working. It is safer to store the value returned by add_submenu_page() in a class property and compare against it, or to check get_current_screen()->id.

#What WP_Screen and get_current_screen are for

The WP_Screen object describes the current panel screen. Its public properties include id, base, parent_base, post_type, taxonomy and action. For the page list id is edit-page, base is edit and post_type is page, so one condition on post_type covers the list, the editor and the add-new screen of a given type.

The object also carries two interface elements that are often written from scratch for no reason: help tabs (add_help_tab()) and screen options (add_option(), for example the number of items per page in a list). Both should be added in the load-{$page_hook} action, because only then does the screen object exist while the header with the “Help” and “Screen Options” buttons has not been printed yet.

#Which body classes an admin screen gets

admin-header.php builds the class attribute of the body tag from several sources. The first class is $hook_suffix with every character outside a-z0-9_- replaced by a hyphen, so a plugin screen gets the class toplevel_page_my-page on body. Then come, among others, post-type-{type} and taxonomy-{taxonomy} from the screen object, admin-color-{scheme} from the user’s profile settings, locale-{language}, branch-{version} and version-{version}, multisite and network-admin on a network, and wp-theme-{theme} with the name of the active theme. Custom classes are added with the admin_body_class filter. This lets plugin CSS target a single screen with a class selector instead of loading a separate stylesheet.

#How hooks work in the WordPress administration panel

Hooks are the mechanism that changes the behavior of the panel without touching WordPress core files.

#Action hooks in the administration panel

Actions run code at a specific moment:

// Add a script only in the administration panel
add_action('admin_enqueue_scripts', function($hook) {
    // $hook holds the identifier of the current page
    if ($hook === 'toplevel_page_my-page') {
        wp_enqueue_script(
            'my-page-js',
            plugin_dir_url(__FILE__) . 'js/admin.js',
            ['jquery'],
            '1.0.0',
            true
        );
    }
});

// Modify the admin head
add_action('admin_head', function() {
    ?>
    <style>
        #wpadminbar { background: #1d2327; }
    </style>
    <?php
});

// Add a notice
add_action('admin_notices', function() {
    ?>
    <div class="notice notice-success is-dismissible">
        <p>Your changes have been saved.</p>
    </div>
    <?php
});

#Filter hooks in the administration panel

Filters change data before it is displayed or saved:

// Modify the panel page title
add_filter('admin_title', function($admin_title, $title) {
    return $title . ' | My Site';
}, 10, 2);

// Change the panel footer text
add_filter('admin_footer_text', function($text) {
    return 'Thank you for creating with <a href="https://wordpress.org">WordPress</a> | Technical support: My Company';
});

// Modify the columns of the post list
add_filter('manage_posts_columns', function($columns) {
    $new_columns = [];
    foreach ($columns as $key => $value) {
        $new_columns[$key] = $value;
        if ($key === 'title') {
            $new_columns['my_column'] = 'My Extra Column';
        }
    }
    return $new_columns;
});

#Which CSS classes the WordPress administration panel uses

WordPress ships a set of CSS classes that keeps the panel looking consistent.

#Container CSS classes in the panel

/* Main page container */
.wrap {
    margin: 10px 20px 0 2px;
}

/* Page heading */
.wrap h1 {
    font-size: 23px;
    font-weight: 400;
    margin: 0;
    padding: 9px 0 4px;
    line-height: 1.3;
}

/* Metaboxes */
.postbox {
    min-width: 255px;
    border: 1px solid #c3c4c7;
    box-shadow: 0 1px 1px rgba(0,0,0,.04);
    background: #fff;
}

/* Forms */
.form-table {
    border-collapse: collapse;
    margin-top: 0.5em;
    width: 100%;
    clear: both;
}

.form-table th {
    vertical-align: top;
    text-align: left;
    padding: 20px 10px 20px 0;
    width: 200px;
    line-height: 1.3;
    font-weight: 600;
}

#Button and UI component CSS classes

/* Buttons (wp-includes/css/buttons.css) */
.wp-core-ui .button-primary {
    background: var(--wp-admin-theme-color, #3858e9);
    border-color: var(--wp-admin-theme-color, #3858e9);
    color: #fff;
    text-decoration: none;
    text-shadow: none;
}

/* Notices (wp-admin/css/common.css) */
.notice {
    background: #fff;
    border: none;
    border-left: 4px solid #c3c4c7;
    box-shadow: none;
    margin: 5px 15px 2px;
    padding: 8px 12px;
}

.notice-success {
    border-left-color: #4ab866;
}

.notice-error {
    border-left-color: #cc1818;
}

The values above come from the buttons.css and common.css files in version 7.1.3. The primary button color is not hard-coded: it comes from the --wp-admin-theme-color CSS variable, which the selected color scheme sets, and #3858e9 is only the fallback value. A plugin that hard-codes the button color in its own stylesheet will clash with the panel of a user who picked a different scheme in their profile. It is safer to use the core classes (button button-primary, notice notice-success) and leave their colors alone.

#How to build custom administration screens

Below is the shortest working example of an options page built on the Settings API. A full guide to tidying menus, client roles, color schemes and dashboard widgets is in a separate article on WordPress admin customization.

#How to add a simple options page

class My_Options_Page {
    
    public function __construct() {
        add_action('admin_menu', [$this, 'add_page']);
        add_action('admin_init', [$this, 'register_settings']);
    }
    
    public function add_page() {
        add_options_page(
            'My Settings',
            'My Settings',
            'manage_options',
            'my-settings',
            [$this, 'render_page']
        );
    }
    
    public function register_settings() {
        register_setting('my_settings_group', 'my_option_1');
        register_setting('my_settings_group', 'my_option_2');
        
        add_settings_section(
            'my_section',
            'Main Settings',
            [$this, 'render_section'],
            'my-settings'
        );
        
        add_settings_field(
            'my_option_1',
            'First Option',
            [$this, 'render_field_1'],
            'my-settings',
            'my_section'
        );
    }
    
    public function render_page() {
        ?>
        <div class="wrap">
            <h1><?php echo esc_html(get_admin_page_title()); ?></h1>
            <form action="options.php" method="post">
                <?php
                settings_fields('my_settings_group');
                do_settings_sections('my-settings');
                submit_button('Save Settings');
                ?>
            </form>
        </div>
        <?php
    }
    
    public function render_field_1() {
        $value = get_option('my_option_1');
        ?>
        <input type="text" name="my_option_1" value="<?php echo esc_attr($value); ?>" class="regular-text">
        <?php
    }
}

new My_Options_Page();

#What is the difference between admin-ajax.php and admin-post.php

The panel has two separate entry points for requests that do not render a screen, and both bypass admin.php.

admin-ajax.php handles AJAX requests. It defines the DOING_AJAX constant, loads wp-load.php and the panel API, sends the X-Robots-Tag: noindex header, fires admin_init, and then the wp_ajax_{action} action for logged-in users or wp_ajax_nopriv_{action} for guests. A missing action parameter ends with the response 0 and status 400, and a missing registered handler also ends with 0 and status 400. On panel screens the address of this file is available right away in the JavaScript variable ajaxurl, which admin-header.php prints. The Plugin Handbook describes the whole flow in the chapter on AJAX in plugins.

admin-post.php is meant for forms sent with a plain POST that should be followed by a redirect. It fires admin_init and then admin_post_{action} for logged-in users or admin_post_nopriv_{action} for guests. If nothing is attached to the action, the file ends with an empty response and status 400. The hook is described on the admin_post_{$action} page.

What the two files share has performance consequences. admin_init runs on every request to admin-ajax.php, including the periodic Heartbeat API requests (the heartbeat action is on the list of core actions, and wp_ajax_nopriv_heartbeat covers guests). Heavy code attached to admin_init without a wp_doing_ajax() check therefore runs not only when a screen opens but on every such tick.

#How the panel differs from the theme template hierarchy

The word “template” in the title of this article is easy to confuse with theme templates. The front end picks a file according to the template hierarchy, which the Theme Handbook describes like this:

“When a visitor lands on any page on a WordPress site, WordPress uses the query string to decide which template should be used to display the page. The query string contains data from the URL that helps make this determination.”

Theme Handbook, Template hierarchy

For block themes the same documentation page lists three places where WordPress looks for a template, in order of priority: user-created templates stored in the database as the wp_template post type, the child theme’s /templates folder and the parent theme’s /templates folder.

The panel has none of these mechanisms. There is no fallback file like a theme’s index.php and no matching by query type. What gets displayed depends on the file name in wp-admin or the page parameter, and the screen layout is always the same header, menu and footer from core. The theme has no say over how the panel looks, apart from its name ending up in the wp-theme-* class on body. That is why panel styles and scripts are enqueued through admin_enqueue_scripts and front-end ones through wp_enqueue_scripts. Mixing up the two actions ends with the admin stylesheet on the front end or the other way round.

#How to secure WordPress administration screens

#Checking user capabilities

Always verify user capabilities:

// Check that the user has the right capabilities
if (!current_user_can('manage_options')) {
    wp_die(__('You do not have permission to access this page.', 'text-domain'));
}

// Check the nonce for the action
if (!isset($_POST['my_nonce']) || !wp_verify_nonce($_POST['my_nonce'], 'my_action')) {
    wp_die(__('Invalid security token.', 'text-domain'));
}

#Sanitizing panel form data

// Text sanitization
$title = sanitize_text_field($_POST['title']);

// HTML sanitization
$content = wp_kses_post($_POST['content']);

// URL sanitization
$url = esc_url_raw($_POST['url']);

// Email sanitization
$email = sanitize_email($_POST['email']);

#How to debug the WordPress administration dashboard

#How to enable WP_DEBUG in the panel

// In wp-config.php
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', false);
define('SCRIPT_DEBUG', true); // Uses unminified scripts

The Debugging in WordPress documentation describes SCRIPT_DEBUG as a constant that makes WordPress use the development versions of core CSS and JavaScript files instead of the minified ones, and points to it as useful when testing changes to the built-in .js and .css files. It defaults to false. In panel terms this means the browser gets common.js instead of wp-admin/js/common.min.js, and a stack trace from the console points to a readable line instead of one long line of the .min file. The same page reminds you that the definitions have to go before the comment that ends editing in wp-config.php.

#Logging panel events to debug.log

function log_admin_action($action, $data) {
    if (defined('WP_DEBUG') && WP_DEBUG) {
        error_log("Admin Action [{$action}]: " . print_r($data, true));
    }
}

// Usage
log_admin_action('user_login', ['user_id' => get_current_user_id()]);

#What to remember about the WordPress administration panel

Five points from this anatomy that matter when writing a plugin:

  1. Directory structure: knowing how the files are organized makes debugging and changes faster.
  2. Template system: callback functions and hooks control how a screen renders.
  3. Security: capability checks and data sanitization are the baseline.
  4. CSS classes: staying consistent with core classes keeps plugin screens in line with the rest of the panel.
  5. Debugging: SCRIPT_DEBUG and the debug log shorten the path to the cause of an error.

Panel modifications survive updates only when they use the built-in mechanisms instead of working around them. We prepare and test such changes on staging as part of WordPress customization.

Next step

Turn the article into an actual implementation

This block strengthens internal linking and gives readers the most relevant next move instead of leaving them at a dead end.

Want this implemented on your site?

If you want to convert the article into a working site improvement, redesign, or build plan, I can define the scope and implement it.

Related cluster

Explore other WordPress services and knowledge base

Strengthen your business with professional technical support in key areas of the WordPress ecosystem.

Frequently asked questions

Practical answers to apply the topic in real execution.

How WordPress assembles wp-admin screens#
Through the wp-admin/admin.php file: it loads core, builds the menu (the admin_menu action), fires admin_init, sets $hook_suffix and the WP_Screen object, then includes admin-header.php, calls the page callback and includes admin-footer.php. The page callback renders only the inside of the screen.
How an admin panel template differs from a front-end theme template#
The front end uses the theme template hierarchy; the admin has its own files and admin_enqueue_scripts scoped to the hook_suffix. Mixing up these layers ends with admin CSS on the front end or the other way round.
Why admin panel anatomy matters when building a custom plugin#
So you do not attach a full Bootstrap to every admin page or register heavy scripts globally. Scoped enqueueing and dedicated templates keep the panel usable on weak hosting.

Need an FAQ tailored to your industry and market? We can build one aligned with your business goals.

Let’s discuss

Related Articles