Introduction to WordPress Admin Interfaces
Well-designed admin interfaces are crucial for plugin usability. WordPress provides APIs for creating seamless admin experiences that integrate with its existing interface.
Think of the WordPress admin area as a dashboard of a vehicle: your plugin's admin interface should feel like native controls that belong on that dashboard, not like aftermarket additions glued on haphazardly.
Creating Admin Menu Pages
WordPress provides several functions to add menu items to the admin dashboard.
Types of Admin Pages
- Top-level menu pages: Appear directly in the main admin menu
- Submenu pages: Appear under existing menu items
- Hidden pages: Not visible in menus but accessible via direct URLs
- Dashboard widgets: Appear on the WordPress dashboard
- Custom columns: Add data to post/page/user list tables
- Meta boxes: Add settings to post/page editing screens
Adding a Top-Level Menu Page
/**
* Add a top-level admin menu page
*/
function my_plugin_add_menu_page() {
add_menu_page(
'My Plugin Settings', // Page title
'My Plugin', // Menu title
'manage_options', // Capability required
'my-plugin-settings', // Menu slug
'my_plugin_settings_page', // Callback function for content
'dashicons-admin-plugins', // Icon (dashicons or URL)
85 // Position in menu (higher = lower)
);
}
add_action('admin_menu', 'my_plugin_add_menu_page');
/**
* Render the settings page content
*/
function my_plugin_settings_page() {
// Check user capabilities
if (!current_user_can('manage_options')) {
return;
}
?>
<div class="wrap">
<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
<form method="post" action="options.php">
<?php
// Output security fields
settings_fields('my_plugin_options');
// Output setting sections and fields
do_settings_sections('my-plugin-settings');
// Output save settings button
submit_button();
?>
</form>
</div>
<?php
}
Adding a Submenu Page
/**
* Add submenu pages under your top-level menu
*/
function my_plugin_add_submenu_pages() {
// First add the top-level menu
add_menu_page(
'My Plugin', // Page title
'My Plugin', // Menu title
'manage_options', // Capability
'my-plugin', // Menu slug
'my_plugin_main_page', // Callback function
'dashicons-admin-plugins', // Icon
85 // Position
);
// Add submenu items
add_submenu_page(
'my-plugin', // Parent slug
'Settings', // Page title
'Settings', // Menu title
'manage_options', // Capability
'my-plugin', // Menu slug (same as parent to override)
'my_plugin_main_page' // Callback function
);
add_submenu_page(
'my-plugin', // Parent slug
'Statistics', // Page title
'Statistics', // Menu title
'manage_options', // Capability
'my-plugin-stats', // Menu slug (different)
'my_plugin_stats_page' // Callback function
);
}
add_action('admin_menu', 'my_plugin_add_submenu_pages');
Adding to Existing WordPress Menus
/**
* Add submenu page to an existing WordPress menu
*/
function my_plugin_add_to_existing_menu() {
// Add to Settings menu
add_options_page(
'My Plugin Options', // Page title
'My Plugin', // Menu title
'manage_options', // Capability
'my-plugin-options', // Menu slug
'my_plugin_options_page' // Callback function
);
// Add to Appearance menu
add_theme_page(
'My Plugin Theme Options', // Page title
'My Plugin Theme', // Menu title
'edit_theme_options', // Capability
'my-plugin-theme', // Menu slug
'my_plugin_theme_page' // Callback function
);
}
add_action('admin_menu', 'my_plugin_add_to_existing_menu');
Understanding the Settings API
The Settings API provides a standardized way to create and manage plugin settings, handle form validation, and store option values.
Think of the Settings API as a form-building framework: it handles the structure, security, and data persistence while you focus on creating the right fields and processing the data.
Core Components of the Settings API
- Settings: Groups of related options (registered with
register_setting()) - Sections: Groups of related fields (added with
add_settings_section()) - Fields: Individual settings to be configured (added with
add_settings_field())
Settings API Flow
- Register settings (validation and sanitization)
- Add settings sections (visual grouping on the page)
- Add settings fields to sections (actual form controls)
- Create callback functions to render settings
- Create the settings page with the form
Implementing the Settings API
Basic Implementation Example
/**
* Register plugin settings, sections, and fields
*/
function my_plugin_register_settings() {
// Register a setting
register_setting(
'my_plugin_options', // Option group
'my_plugin_options', // Option name in database
array(
'sanitize_callback' => 'my_plugin_sanitize_options',
'default' => array(
'text_field' => 'Default text',
'checkbox_field' => 1,
'radio_field' => 'option1'
)
)
);
// Add a settings section
add_settings_section(
'my_plugin_general_section', // Section ID
'General Settings', // Section title
'my_plugin_general_section_cb', // Callback for section intro
'my-plugin-settings' // Page slug
);
// Add fields to the section
add_settings_field(
'text_field', // Field ID
'Text Field', // Field title
'my_plugin_text_field_cb', // Callback to render field
'my-plugin-settings', // Page slug
'my_plugin_general_section', // Section ID
array(
'label_for' => 'text_field',
'class' => 'my-plugin-text-class'
)
);
add_settings_field(
'checkbox_field',
'Checkbox Field',
'my_plugin_checkbox_field_cb',
'my-plugin-settings',
'my_plugin_general_section'
);
}
add_action('admin_init', 'my_plugin_register_settings');
/**
* Section callback
*/
function my_plugin_general_section_cb($args) {
echo '<p>General settings for the plugin functionality.</p>';
}
/**
* Text field callback
*/
function my_plugin_text_field_cb($args) {
// Get saved options
$options = get_option('my_plugin_options');
// Set default value if option doesn't exist
$value = isset($options['text_field']) ? $options['text_field'] : '';
// Output the field
echo '<input type="text" id="text_field" name="my_plugin_options[text_field]" value="' .
esc_attr($value) . '" class="regular-text">';
echo '<p class="description">Enter your text here.</p>';
}
/**
* Checkbox field callback
*/
function my_plugin_checkbox_field_cb($args) {
// Get saved options
$options = get_option('my_plugin_options');
// Set default value if option doesn't exist
$checked = isset($options['checkbox_field']) ? checked(1, $options['checkbox_field'], false) : '';
// Output the field
echo '<input type="checkbox" id="checkbox_field" name="my_plugin_options[checkbox_field]" value="1" ' .
$checked . '>';
echo '<label for="checkbox_field">Enable this feature</label>';
}
/**
* Sanitize options before saving
*/
function my_plugin_sanitize_options($options) {
// Sanitize text field
if (isset($options['text_field'])) {
$options['text_field'] = sanitize_text_field($options['text_field']);
}
// Sanitize checkbox (ensure it's either 1 or 0)
$options['checkbox_field'] = isset($options['checkbox_field']) ? 1 : 0;
return $options;
}
Creating Different Field Types
The Settings API can be used to create a variety of field types:
Text Fields
function my_plugin_text_field_cb($args) {
$options = get_option('my_plugin_options');
$value = isset($options['text_field']) ? $options['text_field'] : '';
echo '<input type="text" id="text_field" name="my_plugin_options[text_field]" value="' .
esc_attr($value) . '" class="regular-text">';
}
Textarea
function my_plugin_textarea_field_cb($args) {
$options = get_option('my_plugin_options');
$value = isset($options['textarea_field']) ? $options['textarea_field'] : '';
echo '<textarea id="textarea_field" name="my_plugin_options[textarea_field]" rows="5" cols="50">' .
esc_textarea($value) . '</textarea>';
}
Select Dropdown
function my_plugin_select_field_cb($args) {
$options = get_option('my_plugin_options');
$value = isset($options['select_field']) ? $options['select_field'] : '';
$items = array(
'option1' => 'Option 1',
'option2' => 'Option 2',
'option3' => 'Option 3'
);
echo '<select id="select_field" name="my_plugin_options[select_field]">';
foreach ($items as $key => $label) {
echo '<option value="' . esc_attr($key) . '"' .
selected($value, $key, false) . '>' .
esc_html($label) . '</option>';
}
echo '</select>';
}
Radio Buttons
function my_plugin_radio_field_cb($args) {
$options = get_option('my_plugin_options');
$value = isset($options['radio_field']) ? $options['radio_field'] : '';
$items = array(
'option1' => 'Option 1',
'option2' => 'Option 2',
'option3' => 'Option 3'
);
foreach ($items as $key => $label) {
echo '<label>';
echo '<input type="radio" name="my_plugin_options[radio_field]" value="' .
esc_attr($key) . '"' . checked($value, $key, false) . '>';
echo esc_html($label);
echo '</label><br>';
}
}
Checkbox Group
function my_plugin_checkboxes_field_cb($args) {
$options = get_option('my_plugin_options');
$values = isset($options['checkboxes_field']) ? $options['checkboxes_field'] : array();
$items = array(
'option1' => 'Feature 1',
'option2' => 'Feature 2',
'option3' => 'Feature 3'
);
foreach ($items as $key => $label) {
$checked = isset($values[$key]) ? checked(1, $values[$key], false) : '';
echo '<label>';
echo '<input type="checkbox" name="my_plugin_options[checkboxes_field][' . esc_attr($key) . ']" value="1"' .
$checked . '>';
echo esc_html($label);
echo '</label><br>';
}
}
Color Picker (using WordPress Color Picker)
// Enqueue the color picker script
function my_plugin_admin_scripts($hook) {
// Only enqueue on our settings page
if ($hook != 'settings_page_my-plugin-settings') {
return;
}
// Add the color picker CSS & JS
wp_enqueue_style('wp-color-picker');
wp_enqueue_script('wp-color-picker');
// Add our custom script
wp_enqueue_script('my-plugin-admin', plugin_dir_url(__FILE__) . 'js/admin.js', array('wp-color-picker'), false, true);
}
add_action('admin_enqueue_scripts', 'my_plugin_admin_scripts');
// Color picker field callback
function my_plugin_color_field_cb($args) {
$options = get_option('my_plugin_options');
$value = isset($options['color_field']) ? $options['color_field'] : '#ffffff';
echo '<input type="text" id="color_field" name="my_plugin_options[color_field]" value="' .
esc_attr($value) . '" class="my-color-field">';
}
// JavaScript for the admin page (js/admin.js)
// jQuery(document).ready(function($) {
// $('.my-color-field').wpColorPicker();
// });
The Settings API is like a set of standard building blocks – once you learn how to create each type of field, you can mix and match them to create complex settings pages.
Creating Tabbed Settings Pages
For more complex plugins, organizing settings into tabs improves usability.
/**
* Create a tabbed settings page
*/
function my_plugin_settings_page() {
if (!current_user_can('manage_options')) {
return;
}
// Get current tab
$default_tab = 'general';
$tab = isset($_GET['tab']) ? sanitize_key($_GET['tab']) : $default_tab;
?>
<div class="wrap">
<h1></h1>
<nav class="nav-tab-wrapper">
<a href="?page=my-plugin-settings&tab=general"
class="nav-tab ">
General
</a>
<a href="?page=my-plugin-settings&tab=advanced"
class="nav-tab ">
Advanced
</a>
<a href="?page=my-plugin-settings&tab=tools"
class="nav-tab ">
Tools
</a>
</nav>
<div class="tab-content">
<form method="post" action="options.php">
</form>
</div>
</div>
'my_plugin_general_options', 'field_name' => 'enable_feature')
);
// Advanced settings tab
register_setting('my_plugin_advanced_options', 'my_plugin_advanced_options');
add_settings_section(
'my_plugin_advanced_section',
'Advanced Settings',
'my_plugin_advanced_section_cb',
'my-plugin-advanced'
);
add_settings_field(
'cache_timeout',
'Cache Timeout (seconds)',
'my_plugin_number_field_cb',
'my-plugin-advanced',
'my_plugin_advanced_section',
array('option_name' => 'my_plugin_advanced_options', 'field_name' => 'cache_timeout')
);
}
add_action('admin_init', 'my_plugin_register_tabbed_settings');
This approach is like organizing a filing cabinet with different drawers (tabs) for different categories of documents, making it easier to find what you need.
Creating Custom Option Pages
Sometimes you need more flexibility than the Settings API provides. You can create custom option pages using your own HTML and AJAX.
Custom Form with AJAX Processing
/**
* Register the custom admin page
*/
function my_plugin_add_custom_page() {
add_menu_page(
'My Custom Page',
'My Custom Page',
'manage_options',
'my-custom-page',
'my_plugin_custom_page_content',
'dashicons-admin-generic',
30
);
}
add_action('admin_menu', 'my_plugin_add_custom_page');
/**
* Render the custom page content
*/
function my_plugin_custom_page_content() {
if (!current_user_can('manage_options')) {
return;
}
// Get existing options
$options = get_option('my_plugin_custom_options', array());
?>
<div class="wrap">
<h1></h1>
<div id="my-plugin-custom-container">
<div class="my-plugin-section">
<h2>Import/Export Settings</h2>
<div class="my-plugin-card">
<h3>Export Settings</h3>
<p>Export your current settings as a JSON file.</p>
<button type="button" id="my-plugin-export" class="button button-primary">
Export Settings
</button>
<div id="my-plugin-export-result"></div>
</div>
<div class="my-plugin-card">
<h3>Import Settings</h3>
<p>Import settings from a JSON file.</p>
<input type="file" id="my-plugin-import-file" accept=".json">
<button type="button" id="my-plugin-import" class="button button-primary">
Import Settings
</button>
<div id="my-plugin-import-result"></div>
</div>
</div>
<div class="my-plugin-section">
<h2>Custom Settings</h2>
<div class="my-plugin-card">
<h3>Dynamic Fields</h3>
<p>Add custom field pairs:</p>
<div id="my-plugin-field-container">
$field) {
?>
<div class="my-plugin-field-pair">
<input type="text" name="field_key[]" value="" placeholder="Key">
<input type="text" name="field_value[]" value="" placeholder="Value">
<button type="button" class="button my-plugin-remove-field">Remove</button>
</div>
</div>
<button type="button" id="my-plugin-add-field" class="button">Add Field</button>
<button type="button" id="my-plugin-save-fields" class="button button-primary">Save Fields</button>
<div id="my-plugin-fields-result"></div>
</div>
</div>
</div>
</div>
admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('my_plugin_ajax_nonce')
));
}
/**
* Process AJAX request to save custom fields
*/
function my_plugin_save_custom_fields() {
// Verify nonce
if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'my_plugin_ajax_nonce')) {
wp_send_json_error('Security check failed');
}
// Check user capabilities
if (!current_user_can('manage_options')) {
wp_send_json_error('Permission denied');
}
// Get and sanitize the data
$keys = isset($_POST['keys']) ? $_POST['keys'] : array();
$values = isset($_POST['values']) ? $_POST['values'] : array();
$custom_fields = array();
for ($i = 0; $i < count($keys); $i++) {
if (!empty($keys[$i])) {
$custom_fields[] = array(
'key' => sanitize_text_field($keys[$i]),
'value' => sanitize_text_field($values[$i])
);
}
}
// Get existing options
$options = get_option('my_plugin_custom_options', array());
// Update with new data
$options['custom_fields'] = $custom_fields;
// Save to database
update_option('my_plugin_custom_options', $options);
wp_send_json_success('Settings saved successfully!');
}
add_action('wp_ajax_my_plugin_save_custom_fields', 'my_plugin_save_custom_fields');
Custom option pages are like building your own custom dashboard controls rather than using the standard ones that came with the vehicle.
Adding Admin Notices
Admin notices provide feedback to users about operations, errors, or important information.
Types of Admin Notices
- Success notices: Green background - operation completed successfully
- Info notices: Blue background - informational message
- Warning notices: Yellow background - heads-up about potential issues
- Error notices: Red background - critical issue requiring attention
/**
* Display admin notices
*/
function my_plugin_admin_notices() {
// Check if we need to show a notice
if (isset($_GET['my_plugin_notice']) && $_GET['my_plugin_notice'] === 'settings_saved') {
?>
<div class="notice notice-success is-dismissible">
<p>Settings saved successfully!</p>
</div>
<div class="notice notice-warning">
<p>
Your plugin requires an API key to function properly.
<a href="">Configure now</a>.
</p>
</div>
Persistent Admin Notices
For notices that need to persist across page loads, use a transient or option:
/**
* Set an admin notice to be displayed
*/
function my_plugin_set_admin_notice($type, $message) {
$notices = get_option('my_plugin_admin_notices', array());
$notices[] = array(
'type' => $type,
'message' => $message
);
update_option('my_plugin_admin_notices', $notices);
}
/**
* Display saved admin notices
*/
function my_plugin_display_admin_notices() {
$notices = get_option('my_plugin_admin_notices', array());
if (empty($notices)) {
return;
}
foreach ($notices as $notice) {
?>
<div class="notice notice- is-dismissible">
<p></p>
</div>
Admin notices are like dashboard warning lights in a car - they draw attention to important information when needed.
Using AJAX in Admin Pages
AJAX enables interactive admin interfaces without page reloads.
Setting Up AJAX in WordPress Admin
/**
* Enqueue the admin scripts
*/
function my_plugin_admin_scripts($hook) {
// Only load on our plugin pages
if (strpos($hook, 'my-plugin') === false) {
return;
}
wp_enqueue_script('my-plugin-admin', plugin_dir_url(__FILE__) . 'js/admin.js', array('jquery'), '1.0.0', true);
// Pass necessary data to JavaScript
wp_localize_script('my-plugin-admin', 'myPluginData', array(
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('my_plugin_ajax_nonce'),
'pluginUrl' => plugin_dir_url(__FILE__),
'i18n' => array(
'confirmDelete' => __('Are you sure you want to delete this item?', 'my-plugin'),
'success' => __('Operation completed successfully', 'my-plugin'),
'error' => __('An error occurred', 'my-plugin')
)
));
}
add_action('admin_enqueue_scripts', 'my_plugin_admin_scripts');
/**
* AJAX handler for data refresh
*/
function my_plugin_ajax_refresh_data() {
// Check nonce
if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'my_plugin_ajax_nonce')) {
wp_send_json_error('Security check failed');
}
// Check capabilities
if (!current_user_can('manage_options')) {
wp_send_json_error('Permission denied');
}
// Process the request
$item_id = isset($_POST['item_id']) ? intval($_POST['item_id']) : 0;
// Get fresh data
$data = my_plugin_get_latest_data($item_id);
if ($data) {
wp_send_json_success(array(
'message' => 'Data refreshed successfully',
'data' => $data
));
} else {
wp_send_json_error('Unable to refresh data');
}
}
add_action('wp_ajax_my_plugin_refresh_data', 'my_plugin_ajax_refresh_data');
/**
* Example function to get latest data
*/
function my_plugin_get_latest_data($item_id) {
// This would be replaced with actual data retrieval logic
return array(
'item_id' => $item_id,
'name' => 'Test Item',
'value' => rand(1, 100),
'last_updated' => current_time('mysql')
);
}
JavaScript for AJAX Interactions
// admin.js
jQuery(document).ready(function($) {
// Handle refresh button click
$('.my-plugin-refresh-button').on('click', function(e) {
e.preventDefault();
var button = $(this);
var itemId = button.data('item-id');
var resultContainer = $('#my-plugin-result-' + itemId);
// Show loading indicator
button.addClass('loading');
resultContainer.html('Loading...');
// Make AJAX request
$.ajax({
url: myPluginData.ajaxUrl,
type: 'POST',
data: {
action: 'my_plugin_refresh_data',
nonce: myPluginData.nonce,
item_id: itemId
},
success: function(response) {
button.removeClass('loading');
if (response.success) {
// Update the UI with new data
var data = response.data;
var html = '<div class="my-plugin-item-data">';
html += '<h4>' + data.name + '</h4>';
html += '<p>Value: ' + data.value + '</p>';
html += '<p>Updated: ' + data.last_updated + '</p>';
html += '</div>';
resultContainer.html(html);
} else {
resultContainer.html('<div class="my-plugin-error">' + response.data + '</div>');
}
},
error: function() {
button.removeClass('loading');
resultContainer.html('<div class="my-plugin-error">' +
myPluginData.i18n.error + '</div>');
}
});
});
});
AJAX in admin pages is like having smart controls in your dashboard that update information in real time without needing to pull over and restart the car.
Best Practices for Admin Interfaces
UX/UI Considerations
- Follow WordPress UI patterns: Make your interface feel familiar to users
- Use WordPress UI components: Buttons, form controls, tabs, etc.
- Group related settings: Organize logically into sections or tabs
- Provide clear descriptions: Help users understand each setting
- Include default values: Choose sensible defaults for all settings
- Add inline validation: Catch errors before form submission
- Keep it simple: Don't overwhelm users with too many options
Security Best Practices
- Always check capabilities: Verify user permissions before processing
- Use nonces: Add security tokens to forms and AJAX requests
- Sanitize all inputs: Clean user input before processing or storing
- Validate and sanitize outputs: Escape data before displaying
- Limit Ajax actions: Only registered users should access admin AJAX
/**
* Security Best Practices - Examples
*/
// 1. Check user capabilities
if (!current_user_can('manage_options')) {
wp_die(__('You do not have sufficient permissions to access this page.', 'my-plugin'));
}
// 2. Use nonces in forms
<form method="post" action="options.php">
<?php
// This adds a nonce and action fields
settings_fields('my_plugin_options');
?>
// Form fields here
</form>
// 3. Verify nonces in custom processing
if (
!isset($_POST['_wpnonce']) ||
!wp_verify_nonce($_POST['_wpnonce'], 'my_plugin_action')
) {
wp_die(__('Security check failed', 'my-plugin'));
}
// 4. Sanitize inputs
$clean_email = sanitize_email($_POST['email']);
$clean_text = sanitize_text_field($_POST['name']);
$clean_url = esc_url_raw($_POST['website']);
$clean_number = absint($_POST['number']);
// 5. Sanitize outputs
echo esc_html($text);
echo esc_attr($attribute);
echo esc_url($url);
echo wp_kses_post($html);
Real-World Example: Complete Settings Page
Let's put it all together with a real-world example of a plugin settings page.
The implementation would include:
- Menu registration with the Admin Menu API
- Settings registration with the Settings API
- Tab navigation using query parameters
- Field validation and sanitization
- AJAX functionality for the Tools tab
- Admin notices for operation feedback
- Security checks throughout
Think of this complete admin interface as a comprehensive vehicle dashboard that provides all the controls and information the driver needs, organized logically and following a consistent design language.
Practical Exercise
Create a Plugin with Admin Settings
- Create a new plugin with a top-level menu page
- Implement the Settings API with at least three different field types:
- Text input field
- Checkbox field
- Select dropdown field
- Add proper sanitization for all fields
- Create a second admin page that displays the stored settings
- Add an admin notice when settings are saved
Advanced Challenge
Extend your plugin to include:
- Tabbed interface with at least two tabs
- AJAX functionality to perform an operation without page reload
- Import/export functionality for settings
- Color picker field using the WordPress color picker