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 🙂 |