Contents
Introduction
What this tutorial covers: a complete, detailed walkthrough for creating a custom WordPress admin dashboard widget with PHP. You will learn basic and advanced approaches: a minimal widget, a widget with settings saved via a form (admin-post), an AJAX-based save, enqueuing admin assets, security and capability checks, localization, removing or repositioning widgets, and common debugging tips. All code examples are ready to drop into a plugin file or adapt to your themes admin code.
Prerequisites and environment
- WordPress (4.7 recommended, but code works with modern 5.x installs).
- Administrator account for testing (widgets are typically admin-only).
- Basic PHP knowledge and familiarity with writing small plugins or adding admin code.
- Optional: access to create a plugin file in wp-content/plugins for clean separation.
Plugin structure and best practice
Prefer packaging dashboard widgets into a small plugin rather than placing logic in theme functions.php. This keeps admin UI consistent across theme changes and is easier to maintain. Essential elements of a plugin that contains a dashboard widget:
- Plugin header block.
- Hooking into wp_dashboard_setup to register widgets.
- Callback(s) that output the widget contents (and optionally a form).
- Handlers for form submissions — use admin-post.php or AJAX endpoints.
- Security: capability checks, nonces, sanitization, escaping.
Minimal dashboard widget (basic example)
This minimal plugin registers a widget and outputs static content. Save as a PHP file in wp-content/plugins (example: dashboard-widget-basic.php), then activate the plugin.
lt?php
/
Plugin Name: Dashboard Widget - Basic Example
Description: Adds a simple custom dashboard widget.
Version: 1.0
Author: Your Name
Text Domain: dw-basic
/
add_action(wp_dashboard_setup, dw_basic_register_widget)
function dw_basic_register_widget() {
wp_add_dashboard_widget(
dw_basic_widget, // Widget slug/ID
__(My Simple Widget, dw-basic), // Title
dw_basic_display // Display callback
)
}
function dw_basic_display() {
// Always escape output appropriately
echo ltpgt . esc_html__(Hello! This is a simple dashboard widget., dw-basic) . lt/pgt
}
How it works
- wp_dashboard_setup action runs during admin initialization for dashboard meta boxes.
- wp_add_dashboard_widget registers the widget. You provide an ID, title, and callback.
- The callback prints the widget contents. Use escaping functions like esc_html or wp_kses_post.
Widget with persistent settings (form admin-post handler)
If you want the widget to include a small settings form (example: text or HTML saved in options), use admin-post.php with an action and handler registered via admin_post_{action}. This pattern avoids processing POST inside output callbacks and handles redirects properly.
lt?php
/
Plugin Name: Dashboard Widget with Settings
Description: Dashboard widget with a settings form saved via admin-post.
Version: 1.0
Author: Your Name
Text Domain: dw-settings
/
add_action(wp_dashboard_setup, dw_settings_register_widget)
add_action(admin_post_dw_save_widget, dw_save_widget_handler) // Handles the form POST
function dw_settings_register_widget() {
wp_add_dashboard_widget(
dw_settings_widget,
__(Custom Notes, dw-settings),
dw_settings_display
)
}
function dw_settings_display() {
// Only show to users who can manage options (example capability)
if (!current_user_can(manage_options)) {
echo ltpgt . esc_html__(You do not have permission to view this widget., dw-settings) . lt/pgt
return
}
content = get_option(dw_widget_content, )
// The form posts to admin-post.php?action=dw_save_widget
echo ltform method=post action= . esc_url(admin_url(admin-post.php)) . gt
echo ltinput type=hidden name=action value=dw_save_widget /gt
wp_nonce_field(dw_save_widget_action, dw_save_widget_nonce) // nonce for security
echo ltpgt
echo ltlabel for=dw_widget_contentgt . esc_html__(Notes (HTML allowed):, dw-settings) . lt/labelgtltbr/gt
echo lttextarea id=dw_widget_content name=dw_widget_content rows=6 cols=30gt . esc_textarea(content) . lt/textareagt
echo lt/pgt
echo ltpgtltinput type=submit class=button button-primary value= . esc_attr__(Save, dw-settings) . /gtlt/pgt
echo lt/formgt
}
function dw_save_widget_handler() {
// Capability check
if (!current_user_can(manage_options)) {
wp_die(__(Unauthorized, dw-settings))
}
// Nonce check
check_admin_referer(dw_save_widget_action, dw_save_widget_nonce)
// Sanitize: allow safe HTML via wp_kses_post
content = isset(_POST[dw_widget_content]) ? wp_kses_post(trim(_POST[dw_widget_content])) :
// Save option
update_option(dw_widget_content, content)
// Redirect back to the previous page (dashboard)
redirect = wp_get_referer() ? wp_get_referer() : admin_url()
wp_safe_redirect(redirect)
exit
}
Notes on this approach
- Use wp_nonce_field in the form and check_admin_referer in the handler.
- Use proper capability checks (e.g., manage_options, or a capability appropriate for your widget).
- Sanitize saved content. For rich text saved from trusted users you can use wp_kses_post. For plain text use sanitize_text_field or esc_textarea on output.
- Redirect after saving to avoid double POST on refresh.
AJAX-based saving (better UX)
AJAX lets you save widget data without a full page reload. Use the wp_ajax_ action for logged-in AJAX requests and create a small admin script. Below are the essential plugin PHP and a matching JS snippet.
lt?php
/
Plugin Name: Dashboard Widget AJAX Save
Description: Adds a dashboard widget that saves via AJAX.
Version: 1.0
Author: Your Name
Text Domain: dw-ajax
/
add_action(wp_dashboard_setup, dw_ajax_register_widget)
add_action(admin_enqueue_scripts, dw_ajax_admin_assets) // enqueue script on admin
add_action(wp_ajax_dw_save_ajax, dw_ajax_save_handler) // AJAX handler
function dw_ajax_register_widget() {
wp_add_dashboard_widget(dw_ajax_widget, __(AJAX Widget, dw-ajax), dw_ajax_display)
}
function dw_ajax_display() {
if (!current_user_can(manage_options)) {
echo ltpgt . esc_html__(No permission, dw-ajax) . lt/pgt
return
}
content = get_option(dw_ajax_content, )
echo lttextarea id=dw-ajax-content rows=5 cols=35gt . esc_textarea(content) . lt/textareagtnbsp
echo ltbutton id=dw-ajax-save class=buttongt . esc_html__(Save via AJAX,dw-ajax) . lt/buttongt
echo ltdiv id=dw-ajax-status style=display:inline-blockmargin-left:10pxgtlt/divgt
}
function dw_ajax_admin_assets(hook) {
// The dashboard pages hook is index.php - only load on dashboard
if (hook !== index.php) {
return
}
wp_enqueue_script(dw-ajax-admin, plugin_dir_url(__FILE__) . dw-ajax-admin.js, array(jquery), 1.0, true)
wp_localize_script(dw-ajax-admin, dwAjax, array(
ajax_url => admin_url(admin-ajax.php),
nonce => wp_create_nonce(dw_ajax_nonce),
))
}
function dw_ajax_save_handler() {
check_ajax_referer(dw_ajax_nonce, nonce)
if (!current_user_can(manage_options)) {
wp_send_json_error(__(Unauthorized, dw-ajax))
}
content = isset(_POST[content]) ? wp_kses_post(_POST[content]) :
update_option(dw_ajax_content, content)
wp_send_json_success(__(Saved, dw-ajax))
}
// File: dw-ajax-admin.js
jQuery(function(){
(#dw-ajax-save).on(click, function(e){
e.preventDefault()
var content = (#dw-ajax-content).val()
(#dw-ajax-status).text(Saving...)
.post(dwAjax.ajax_url, {
action: dw_save_ajax,
nonce: dwAjax.nonce,
content: content
}, function(response){
if (response response.success) {
(#dw-ajax-status).text(response.data Saved)
} else {
var msg = response response.data ? response.data : Error
(#dw-ajax-status).text(msg)
}
})
})
})
Security and UX considerations for AJAX
- Always use check_ajax_referer with a nonce.
- Use capability checks server-side.
- Provide user feedback in the UI for success or failure.
- Only enqueue the JS on the dashboard to avoid extra load in other admin pages.
Advanced: Add meta box variant (control context and priority)
If you need more control over context and priority (which column/section the box appears in), use add_meta_box with the screen dashboard. This gives context values and priority control similar to other admin pages.
add_action(wp_dashboard_setup, dw_meta_box_widget)
function dw_meta_box_widget() {
// screen: dashboard, context: side or normal, priority: highdefaultlow
add_meta_box(
dw_meta_widget,
__(Meta Box Widget, dw-domain),
dw_meta_widget_display,
dashboard, // screen
side, // context: side normal column3 (depends on WP version)
high // priority
)
}
function dw_meta_widget_display() {
echo ltpgt . esc_html__(This is a meta_box-style dashboard widget., dw-domain) . lt/pgt
}
Removing or reordering default dashboard widgets
WordPress ships with several default dashboard widgets. Use remove_meta_box to remove them (hook into wp_dashboard_setup). Some common widget IDs include dashboard_quick_press, dashboard_primary, dashboard_right_now, and dashboard_activity (IDs can vary by WP version/plugins).
add_action(wp_dashboard_setup, dw_remove_default_dashboard_widgets)
function dw_remove_default_dashboard_widgets() {
remove_meta_box(dashboard_quick_press, dashboard, side) // Quick Draft / Quick Press
remove_meta_box(dashboard_primary, dashboard, side) // WordPress events/news
// Remove other meta boxes as needed...
}
Security checklist (must-haves)
- Capability checks: use current_user_can to verify a user has the right capability before showing or handling sensitive actions.
- Nonces: use wp_nonce_field in forms and verify with check_admin_referer. For AJAX use check_ajax_referer.
- Sanitize input: sanitize_text_field for plain text wp_kses_post for HTML from trusted users.
- Escape output: esc_html, esc_textarea, esc_attr, or wp_kses_post depending on the context.
- Redirect safely: use wp_safe_redirect when sending users elsewhere after POSTs.
Localization (internationalization)
To make strings translatable, wrap them in __(), _e(), esc_html__(), etc., and set a text domain. Optionally call load_plugin_textdomain() on plugins_loaded or use modern standards like plugin_textdomain and translation files placed in the plugin’s languages folder.
Performance UX tips
- Load admin JS/CSS only on the dashboard page (check the hook parameter in admin_enqueue_scripts – dashboard is index.php).
- Cache heavy widget data if you call external APIs use transients to reduce API calls.
- Keep widget markup minimal to avoid layout thrashing in the admin screen.
- Use non-blocking saves (AJAX) for better UX. Show inline validation and status messages.
Troubleshooting common issues
- Widget not appearing: Ensure the plugin is activated and your code hooks into wp_dashboard_setup. Confirm no fatal errors in PHP logs. Check that the current user has the capability required to view the widget.
- Form POST results in blank page or not saved: Use admin-post.php and add an admin_post_ action. Confirm nonce name and check parameter names match between the form and handler.
- AJAX returns 0 or Error: In WP AJAX, a bare 0 usually means a PHP fatal or permission/nonce failure. Check PHP error logs and confirm check_ajax_referer and capability checks.
- Words in the widget are not translated: Ensure you used translation functions and set the correct text domain. Load textdomain in plugin init if needed.
Useful function references
- wp_add_dashboard_widget(id, title, callback) — register a simple dashboard widget.
- add_meta_box(id, title, callback, screen, context, priority) — register a meta box with context/priority control use screen = dashboard for dashboard meta boxes.
- remove_meta_box(id, screen, context) — remove a dashboard meta box.
- admin_post_{action} — hook for handling non-AJAX form submissions to admin-post.php.
- wp_ajax_{action} — hook for handling logged-in AJAX requests.
Example: Full small plugin combining many elements
The following example is a concise plugin that registers a widget with a form posting to admin-post, with nonce, capability checks, saving to an option, and a redirect back to the dashboard. Combine and adapt pieces from earlier examples to suit more advanced needs.
lt?php
/
Plugin Name: Dashboard Notes
Description: Custom dashboard widget for admin notes. Demonstrates form handling via admin-post and security best practices.
Version: 1.0
Author: Your Name
Text Domain: dw-notes
/
if ( ! defined( ABSPATH ) ) {
exit
}
add_action(wp_dashboard_setup, dw_notes_register)
add_action(admin_post_dw_notes_save, dw_notes_save_handler)
function dw_notes_register() {
wp_add_dashboard_widget(
dw_notes_widget,
__(Admin Notes, dw-notes),
dw_notes_display
)
}
function dw_notes_display() {
if (!current_user_can(manage_options)) {
echo ltpgt . esc_html__(Insufficient permissions., dw-notes) . lt/pgt
return
}
notes = get_option(dw_notes_content, )
echo ltform action= . esc_url(admin_url(admin-post.php)) . method=postgt
echo ltinput type=hidden name=action value=dw_notes_save /gt
wp_nonce_field(dw_notes_save_action, dw_notes_nonce)
echo ltpgtlttextarea name=dw_notes_content rows=6 cols=40gt . esc_textarea(notes) . lt/textareagtlt/pgt
echo ltpgtltinput type=submit class=button button-primary value= . esc_attr__(Save Notes, dw-notes) . /gtlt/pgt
echo lt/formgt
}
function dw_notes_save_handler() {
if (!current_user_can(manage_options)) {
wp_die(__(Unauthorized, dw-notes))
}
check_admin_referer(dw_notes_save_action, dw_notes_nonce)
notes = isset(_POST[dw_notes_content]) ? wp_kses_post(trim(_POST[dw_notes_content])) :
update_option(dw_notes_content, notes)
// Use wp_get_referer to go back to the dashboard
redirect = wp_get_referer() ? wp_get_referer() : admin_url()
wp_safe_redirect(redirect)
exit
}
Checklist before shipping a dashboard widget
- Have you added capability checks so only intended users see/submit data?
- Are all inputs sanitized and all outputs escaped?
- Are nonces in place for forms and AJAX?
- Is the widget localized with proper text domain usage?
- Are admin assets enqueued only on relevant pages?
- Did you consider caching or transient use for heavy external data?
Final notes and recommendations
Dashboard widgets are a lightweight way to add admin-specific functionality. For anything user-editable, follow the security checklist strictly. Prefer AJAX for small, frequent updates to improve the user experience. Package your widget as a plugin for portability and maintenance. Test with different administrator-level accounts and WP installs to ensure compatibility.
Further exploration
- Use the WordPress REST API to save or retrieve dashboard data from external services.
- Combine widgets with custom post types for richer admin UIs.
- Explore third-party libraries for richer editors (e.g., TinyMCE) but ensure proper sanitization when saving.
- Consider placing widgets behind feature flags or options to allow site owners to enable/disable them easily.
|
|
Acepto donaciones de BAT's mediante el navegador Brave 🙂 |
