Contents
Overview
This tutorial shows everything needed to create custom and dismissible WordPress admin notices in PHP. It covers the native WordPress notice markup, how to control where and when notices appear, how to make notices dismissible either temporarily (query-string) or persistently (per-user, via AJAX), accessibility considerations, security (nonces and capability checks), multisite nuances, styling and cleanup, and full working code examples you can drop into a plugin or mu-plugin.
Anatomy of an Admin Notice
WordPress admin notices are simple HTML elements output by hooks like admin_notices, all_admin_notices, or conditionally on specific admin screens. WordPress core provides CSS classes to style them:
- notice — required base class
- notice-success, notice-error, notice-warning, notice-info — visual types
- is-dismissible — makes it dismissible using WPs JS (adds an X)
Basic example markup (for reference):
ltdiv class=notice notice-success is-dismissiblegt ltpgtThis is a success message.lt/pgt lt/divgt
Where to Output Notices
- admin_notices — run on most admin pages (after screen header, before main page content)
- all_admin_notices — run even when youre on a plugin/theme page ordering differs slightly
- Alternatively, inside a specific admin page callback (for plugin pages), output markup directly
To limit notices to a particular admin page, use get_current_screen() or check _GET[page] or hook into the page-specific load action (load-{hook_suffix}).
Basic Non-dismissible Notice Example
/ Show a basic non-dismissible admin notice. / add_action( admin_notices, myplugin_basic_notice ) function myplugin_basic_notice() { if ( ! current_user_can( manage_options ) ) { return } echo ltdiv class=notice notice-infogt echo ltpgt . esc_html__( This is a simple info notice., my-plugin-textdomain ) . lt/pgt echo lt/divgt }
Simple Query-String Dismissal (Non-persistent)
This approach adds a dismiss link that appends a query arg to the URL. It is easy but not persistent per-user across sessions unless you store the dismissed state.
/ Display a notice that can be dismissed by appending a query arg. / add_action( admin_notices, myplugin_query_dismissible_notice ) function myplugin_query_dismissible_notice() { if ( ! current_user_can( manage_options ) ) { return } // Only show if not dismissed this page-load by query arg. if ( isset( _GET[myplugin_dismiss_notice] ) 1 === _GET[myplugin_dismiss_notice] ) { return } dismiss_url = esc_url( add_query_arg( myplugin_dismiss_notice, 1 ) ) echo ltdiv class=notice notice-warninggt echo ltpgt . esc_html__( Please note this important info. , my-plugin-textdomain ) . lta href= . dismiss_url . gt . esc_html__( Dismiss, my-plugin-textdomain ) . lt/agtlt/pgt echo lt/divgt }
Persistent Dismissal: Per-user with update_user_meta is-dismissible
To persist dismissal so an individual user does not see the notice again, store a flag in user meta (or options for a global dismissal). Use AJAX to dismiss without reloading. Below are the pieces youll need: output, enqueue script, AJAX handler and security via nonces.
1) Output the Notice Only If Not Dismissed
add_action( admin_notices, myplugin_persistent_notice ) function myplugin_persistent_notice() { if ( ! current_user_can( manage_options ) ) { return } user_id = get_current_user_id() dismissed = get_user_meta( user_id, myplugin_notice_dismissed, true ) if ( dismissed ) { return // user already dismissed } // print the notice with a data attribute for JS to pick up echo ltdiv id=myplugin-notice class=notice notice-success is-dismissiblegt echo ltpgt . esc_html__( Persistent notice you can dismiss., my-plugin-textdomain ) . lt/pgt echo lt/divgt }
2) Enqueue JavaScript and Localize Data
add_action( admin_enqueue_scripts, myplugin_enqueue_admin_assets ) function myplugin_enqueue_admin_assets( hook ) { // Only load on admin pages where its relevant. Adjust condition as needed. wp_enqueue_script( myplugin-admin-notice, plugin_dir_url( __FILE__ ) . js/admin-notice.js, array( jquery ), 1.0, true ) wp_localize_script( myplugin-admin-notice, MyPluginNotice, array( ajax_url => admin_url( admin-ajax.php ), nonce => wp_create_nonce( myplugin_dismiss_nonce ), ) ) }
3) AJAX Handler to Persist Dismissal
add_action( wp_ajax_myplugin_dismiss_notice, myplugin_ajax_dismiss_notice ) function myplugin_ajax_dismiss_notice() { // Security: check nonce capability check_ajax_referer( myplugin_dismiss_nonce, nonce ) if ( ! current_user_can( manage_options ) ) { wp_send_json_error( array( message =gt Unauthorized ), 403 ) } user_id = get_current_user_id() if ( ! user_id ) { wp_send_json_error( array( message =gt No user ), 400 ) } update_user_meta( user_id, myplugin_notice_dismissed, 1 ) wp_send_json_success() }
4) JavaScript: Trigger AJAX When Dismissed
( function( ) { ( document ).on( click, #myplugin-notice .notice-dismiss, function() { .post( MyPluginNotice.ajax_url, { action: myplugin_dismiss_notice, nonce: MyPluginNotice.nonce } ) } ) } )( jQuery )
Notes:
- We hook to the .notice-dismiss button (WordPress core inserts it for is-dismissible). That ensures unobtrusive behavior.
- We use wp_ajax_{action} to handle the request, protect with check_ajax_referer, and require an appropriate capability.
- Using update_user_meta keeps dismissal per-user use update_option for global dismissal (be careful with capabilities/intent).
Alternative: Use a REST Endpoint
Instead of admin-ajax, you can register a REST endpoint and call it with wp.apiFetch or fetch(). This carries the same security principles (nonce or application passwords/capabilities) but uses the REST layer. Example registration:
add_action( rest_api_init, function() { register_rest_route( myplugin/v1, /dismiss-notice, array( methods =gt POST, callback =gt myplugin_rest_dismiss_notice, permission_callback =gt function() { return current_user_can( manage_options ) }, ) ) } ) function myplugin_rest_dismiss_notice( request ) { user_id = get_current_user_id() update_user_meta( user_id, myplugin_notice_dismissed, 1 ) return rest_ensure_response( array( success =gt true ) ) }
Accessibility and Best Practices
- Wrap meaningful information inside ltpgt for screen readers rather than inline-only elements.
- Use esc_html__ / esc_html_e when outputting translatable text and ensure any HTML is escaped or output with proper sanitization if needed.
- For dynamic updates, consider using aria-live regions (Polite) if the notice updates without a page reload. Example: ltdiv role=status aria-live=politegt (WordPress already communicates many changes, but you can add role if needed).
- Do not rely on CSS-only dismissal ensure server-side persistence if users expect a permanent dismissal.
Security: Nonces, Capability Checks, and Escaping
- Always verify user capabilities (e.g., current_user_can( manage_options )) before showing notices intended for admins or taking actions on dismiss.
- Protect AJAX/REST endpoints with nonces or check permissions in permission_callback. Use check_ajax_referer for admin-ajax endpoints.
- Escape all output: esc_html__ for plain text, wp_kses_post if allowing limited HTML, esc_url for links.
Multisite Considerations
- Decide whether dismissal is per-user networkwide or per-site. In multisite, a user can have different metadata per-site or a global setting in the main site. Use update_user_meta with a site-agnostic key (same key is used across sites) for per-network behavior if thats acceptable.
- For network-admin notices, hook into network_admin_notices and use appropriate capability checks like current_user_can( manage_network ).
Styling and Custom Classes
To customize the visual look beyond default WP classes, add your own class to the notice markup and enqueue an admin stylesheet to override or extend styles. Example class: myplugin-custom-notice. Avoid styling that breaks WP admin UI or reduces accessibility contrast.
Conditional Display: Only on Certain Screens
add_action( admin_notices, myplugin_notice_on_specific_screen ) function myplugin_notice_on_specific_screen() { screen = get_current_screen() // Example: only show on post edit screens if ( ! in_array( screen-gtid, array( post, post-new ), true ) ) { return } echo ltdiv class=notice notice-info is-dismissiblegtltpgtScreen-specific notice.lt/pgtlt/divgt }
Cleaning Up on Deactivation/Uninstall
If you store persistent flags (options or user meta), clean them up on plugin uninstall to avoid leaving orphans. Use the uninstall.php file or register_uninstall_hook.
// uninstall.php if ( ! defined( WP_UNINSTALL_PLUGIN ) ) { exit } // Remove user meta keys across all users (caution: expensive on large sites) users = get_users( array( fields =gt ID ) ) foreach ( users as user_id ) { delete_user_meta( user_id, myplugin_notice_dismissed ) } // If you used an option, delete it too delete_option( myplugin_global_notice_dismissed )
Complete Self-contained Example (Plugin Friendly)
Drop the following into a plugin file (single-file plugin) and create the referenced JS file in a js/ folder. This example shows a per-user persistent dismissible admin notice using admin-ajax and proper sanitization and capability checks.
id ) { return } // Print notice markup with an ID. echo ltdiv id=myplugin-demo-notice class=notice notice-success is-dismissiblegt echo ltpgt . esc_html__( Welcome — this is an important notice. Click the X to dismiss it permanently., my-plugin-textdomain ) . lt/pgt echo lt/divgt } / Enqueue admin script to handle dismissal. / add_action( admin_enqueue_scripts, myplugin_demo_enqueue_assets ) function myplugin_demo_enqueue_assets( hook ) { // Only enqueue where the notice might appear here we enqueue everywhere in admin. wp_enqueue_script( myplugin-demo-admin, plugin_dir_url( __FILE__ ) . js/myplugin-admin.js, array( jquery ), 1.0, true ) wp_localize_script( myplugin-demo-admin, MyPluginDemo, array( ajax_url =gt admin_url( admin-ajax.php ), nonce =gt wp_create_nonce( myplugin_demo_dismiss_nonce ), ) ) } / AJAX handler to mark the notice dismissed. / add_action( wp_ajax_myplugin_demo_dismiss, myplugin_demo_ajax_dismiss ) function myplugin_demo_ajax_dismiss() { check_ajax_referer( myplugin_demo_dismiss_nonce, nonce ) if ( ! current_user_can( manage_options ) ) { wp_send_json_error( array( message =gt __( Unauthorized, my-plugin-textdomain ) ), 403 ) } user_id = get_current_user_id() if ( ! user_id ) { wp_send_json_error( array( message =gt __( No user, my-plugin-textdomain ) ), 400 ) } update_user_meta( user_id, myplugin_demo_notice_dismissed, 1 ) wp_send_json_success() }
Create the file js/myplugin-admin.js alongside the plugin file with this JavaScript:
( function( ) { ( document ).on( click, #myplugin-demo-notice .notice-dismiss, function() { .post( MyPluginDemo.ajax_url, { action: myplugin_demo_dismiss, nonce: MyPluginDemo.nonce } ) } ) } )( jQuery )
Troubleshooting Debugging
- If the notice never appears, verify capability checks and whether the user meta key already exists.
- If dismissal doesn’t persist, confirm the AJAX request is sent (use browser DevTools network tab) and returns success. Check PHP error logs for fatal errors in AJAX handler.
- If the dismiss button isnt present, ensure the notice has the class is-dismissible and WP admin JS is loading (it usually is by default in admin).
- Make sure scripts are enqueued in admin context — admin_enqueue_scripts is the correct hook.
Advanced Patterns
- Show a sequence of notices and track which ones a user has dismissed (store an array in user meta and check membership).
- Use transients for temporary notices (expire automatically after a time).
- Trigger notices from CRON jobs by setting an option or transient and rendering it in admin. Ensure you sanitize data written by CRON.
- Create contextual action links in notices (buttons) and validate targets with nonces. For actions beyond dismissal (e.g., Run Setup), consider showing a confirmation step.
Key Takeaways
- Use notice classes and is-dismissible for consistent admin UI.
- Protect actions via nonces and capability checks.
- Persist dismissal using update_user_meta (per-user) or update_option (global) depending on your needs.
- Prefer admin-ajax or REST endpoints to dismiss notices without page reload, but always validate on the server.
- Respect accessibility and localization when producing text and interactions in notices.
Further Reading
- Administration Menus (WordPress Developer Handbook)
- Plugin Best Practices
- admin_notices hook reference
|
Acepto donaciones de BAT's mediante el navegador Brave 🙂 |