The WordPress admin dashboard is the first screen every editor, author, and administrator sees after logging in — which makes it prime real estate for surfacing useful information quickly. Out of the box WordPress ships with widgets for At a Glance, Activity, Quick Draft, and WordPress News, but for client sites or custom plugins you almost always want to add your own panels. wp_add_dashboard_widget() is the official function for registering a new dashboard widget: it accepts a unique ID, a display title, a callback that renders the HTML, and an optional control callback for the widget’s options form. The function must be called inside a hook on wp_dashboard_setup, which fires after WordPress has set up its own widgets so yours can be added, reordered, or used to remove default widgets in the same callback. Dashboard widgets support the same drag-and-drop repositioning as all WordPress metaboxes — position and open/closed state are saved per user in wp_usermeta. You can pass contextual data into the widget callback by storing it in a $args array passed as the fourth parameter to wp_add_dashboard_widget(). Common uses include a widget that counts posts by status, one that shows the five most recent orders from WooCommerce, a quick-link panel to frequently used admin screens, or a recent activity feed from an external API. Always cap database queries inside dashboard widgets and cache the results with the Transients API — the dashboard loads on every admin page view for logged-in users, so a slow widget degrades the entire backend experience. You can also remove default widgets with remove_meta_box() inside the same wp_dashboard_setup hook. Pair this with the custom admin notices guide for a complete admin customisation toolkit.
Problem: You want to display useful site-specific information — such as recent orders, post counts by status, or quick admin links — directly on the WordPress admin dashboard without using a plugin.
Solution: Hook into wp_dashboard_setup and register your widget with wp_add_dashboard_widget():
add_action( 'wp_dashboard_setup', 'ha_register_dashboard_widgets' );
function ha_register_dashboard_widgets() {
wp_add_dashboard_widget(
'ha_site_summary',
'Site Summary',
'ha_site_summary_widget'
);
// Remove the default WordPress News widget
remove_meta_box( 'dashboard_primary', 'dashboard', 'side' );
}
function ha_site_summary_widget() {
// Cache the counts for 5 minutes to avoid repeated DB hits
$counts = get_transient( 'ha_dashboard_counts' );
if ( false === $counts ) {
$counts = [
'published' => wp_count_posts()->publish,
'draft' => wp_count_posts()->draft,
'pages' => wp_count_posts( 'page' )->publish,
'comments' => get_comments( [ 'count' => true, 'status' => 'approve' ] ),
];
set_transient( 'ha_dashboard_counts', $counts, 5 * MINUTE_IN_SECONDS );
}
echo '<ul class="ha-summary">';
echo '<li>Published posts: <strong>' . absint( $counts['published'] ) . '</strong></li>';
echo '<li>Draft posts: <strong>' . absint( $counts['draft'] ) . '</strong></li>';
echo '<li>Published pages: <strong>' . absint( $counts['pages'] ) . '</strong></li>';
echo '<li>Approved comments: <strong>' . absint( $counts['comments'] ) . '</strong></li>';
echo '</ul>';
// Quick-link row
echo '<p>';
echo '<a href="' . esc_url( admin_url( 'post-new.php' ) ) . '">New Post</a> | ';
echo '<a href="' . esc_url( admin_url( 'upload.php' ) ) . '">Media</a> | ';
echo '<a href="' . esc_url( admin_url( 'options-general.php' ) ) . '">Settings</a>';
echo '</p>';
}
NOTE: Dashboard widget IDs must be unique across all registered widgets — prefix them with your theme or plugin slug to avoid conflicts with other plugins. The widget callback receives no arguments by default; if you need to pass data in, use the fourth $args parameter of wp_add_dashboard_widget() and retrieve it via $args['args'] inside the callback. To control which user roles see the widget, wrap the wp_add_dashboard_widget() call in a current_user_can() check.