Skip to content
Back to skills

Wp Admin Notices

ASecurity

Render WordPress admin notices via the four core hooks

  • 22 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added June 5, 2026
developmentgophpreactapisecurity

Works with

  • cli
  • api

Security analysis

A100/100

Scanned June 5, 2026

npx -y skills add Lonsdale201/wp-agent-skills --skill wp-admin-notices --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Wp Admin Notices?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Wp Admin Notices
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/lonsdale201-wp-admin-notices/badge)](https://www.skillsdirectory.com/skills/lonsdale201-wp-admin-notices)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: wp-admin-notices
description: Render WordPress admin notices via the four core hooks
  (`admin_notices`, `network_admin_notices`, `user_admin_notices`,
  `all_admin_notices`) and the 6.4+ `wp_admin_notice()` /
  `wp_get_admin_notice()` helpers. Covers the four severity classes
  (`notice-error/-warning/-info/-success`), `is-dismissible`,
  per-user persisted dismissal via `user_meta` + REST endpoint, screen
  targeting via `get_current_screen()`, transient-backed flash notices
  after redirects, and the `wp_admin_notice_args` /
  `wp_admin_notice_markup` filters. Use for onboarding banners, post-save
  flashes, integration warnings, version-bump tours, or config nags.
author: Soczó Kristóf
contact: mailto:lonsdale201@hotmail.com
plugin: wordpress
plugin-version-tested: "6.4 - 7.0"
php-min: "7.4"
last-updated: "2026-05-24"
docs:
  - https://developer.wordpress.org/reference/functions/wp_admin_notice/
  - https://developer.wordpress.org/reference/functions/wp_get_admin_notice/
  - https://developer.wordpress.org/reference/hooks/admin_notices/
---

# WordPress Admin Notices

The colored banner panels that appear above plugin admin pages. WP 6.4 added a proper helper (`wp_admin_notice()` / `wp_get_admin_notice()`) so you don't have to hand-write the `<div class="notice notice-X is-dismissible">` markup anymore. Most plugins still do. This skill is the modern recipe + the dismissal-persistence pattern that's the hard part.

## When to use this skill

Trigger when ANY of the following is true:

- A plugin needs an admin banner: post-save success flash, integration warning, missing-config nag, version-bump tour, or first-run instructions.
- Code references `admin_notices`, `network_admin_notices`, `user_admin_notices`, `all_admin_notices`, `wp_admin_notice`, `wp_get_admin_notice`, `notice-error`, `notice-warning`, `notice-info`, `notice-success`, `is-dismissible`, `wp_admin_notice_args`.
- The user wants a "dismiss forever" button on a notice and is reaching for cookies / localStorage when user-meta is the right place.
- The user complains: "my notice shows on every admin page" / "I dismissed it but it came back".

## The four hooks — pick by audience

`wp-admin/admin-header.php:290-321` uses `if (is_network_admin()) / elseif (is_user_admin()) / else` to fire **exactly one** of the three context-specific hooks (at lines 299, 306, 313), then ALWAYS fires `all_admin_notices` at line 321 in addition. So on any given admin page render you get **two** hook fires: one context-specific + `all_admin_notices`.

| Hook | Fires when | Use when |
|---|---|---|
| `network_admin_notices` | `is_network_admin()` is true | Multisite Network Admin screens |
| `user_admin_notices` | `is_user_admin()` is true | "My Sites" / user-admin screens |
| `admin_notices` | Standard admin (neither of the above) | Almost every plugin notice |
| `all_admin_notices` | Always, in ADDITION to whichever of the three above ran | Cross-context notices (security warnings, license expired) |

Most plugins want **`admin_notices`** unless they have a clear reason for one of the others.

## Render markup the WP 6.4+ way

`wp_admin_notice( $message, $args )` (defined at `wp-includes/functions.php:9189`) emits the full `<div>` wrapper with the right classes and runs `wp_kses_post()` on the markup. Args (verified at `:9078`):

| Key | Type | Notes |
|---|---|---|
| `type` | string | `'error'`, `'warning'`, `'info'`, `'success'` — adds `notice-<type>` class. No spaces (WP throws `_doing_it_wrong`) |
| `dismissible` | bool | Adds `is-dismissible` (core JS handles the close button) |
| `id` | string | Sets `id="..."` for targeting from JS |
| `additional_classes` | string[] | Extra classes joined into the wrapper |
| `attributes` | array | Extra attrs on the wrapper |
| `paragraph_wrap` | bool | Default `true` — wraps message in `<p>` |

```php
add_action( 'admin_notices', static function (): void {
    if ( empty( get_option( 'myplugin_api_key' ) ) ) {
        wp_admin_notice(
            sprintf(
                /* translators: %s: settings page URL */
                __( 'My Plugin needs an API key. <a href="%s">Configure now</a>.', 'myplugin' ),
                esc_url( admin_url( 'admin.php?page=myplugin' ) )
            ),
            array(
                'type'        => 'warning',
                'dismissible' => false,
                'id'          => 'myplugin-api-key-missing',
            )
        );
    }
} );
```

If you specifically need just the markup string (e.g. to inject from a custom render path), use `wp_get_admin_notice()` with the same args — it returns the HTML without echoing. When you echo that string yourself, pass it through `wp_kses_post()` and keep dynamic `id` / attribute values sanitized.

## Targeting the right screen

`admin_notices` runs on every admin page. Without filtering you spam the user across the whole admin. Use `get_current_screen()` to narrow:

```php
add_action( 'admin_notices', static function (): void {
    $screen = get_current_screen();
    if ( ! $screen || 'toplevel_page_myplugin' !== $screen->id ) {
        return;
    }
    wp_admin_notice( __( 'Settings imported.', 'myplugin' ), array( 'type' => 'success' ) );
} );
```

Common screen IDs: `dashboard`, `plugins`, `edit-post`, `edit-{cpt}`, `post`, `post-new`, `toplevel_page_{slug}`, `settings_page_{slug}`, `{parent}_page_{slug}`.

## Persisted dismissal (per-user)

`is-dismissible` adds an X button but core ONLY hides the notice in the DOM — next page load shows it again. For "dismiss forever" persistence, store a flag in user meta and bail before rendering.

```php
// 1. Don't render if the user dismissed.
add_action( 'admin_notices', static function (): void {
    $user_id = get_current_user_id();
    if ( ! $user_id || get_user_meta( $user_id, 'myplugin_dismissed_v2_intro', true ) ) {
        return;
    }
    wp_admin_notice(
        __( 'My Plugin v2 is here. <a href="#" class="myplugin-dismiss-intro">Got it</a>', 'myplugin' ),
        array( 'type' => 'info', 'dismissible' => true, 'id' => 'myplugin-v2-intro' )
    );
} );

// 2. REST endpoint to record dismissal.
add_action( 'rest_api_init', static function (): void {
    register_rest_route( 'myplugin/v1', '/dismiss-notice/(?P<slug>[a-z0-9_-]+)', array(
        'methods'             => WP_REST_Server::EDITABLE,
        'permission_callback' => static fn () => is_user_logged_in(),
        'callback'            => static function ( WP_REST_Request $req ) {
            $slug = sanitize_key( $req['slug'] );
            update_user_meta( get_current_user_id(), 'myplugin_dismissed_' . $slug, 1 );
            return rest_ensure_response( array( 'ok' => true ) );
        },
    ) );
} );
```

```js
// 3. Client-side: capture either the built-in close button OR your "Got it" link.
// Enqueue this script with deps: array( 'wp-api-fetch' ).
jQuery( function ( $ ) {
    $( document ).on( 'click', '#myplugin-v2-intro .notice-dismiss, .myplugin-dismiss-intro', function ( event ) {
        event.preventDefault();
        wp.apiFetch( {
            path:   '/myplugin/v1/dismiss-notice/v2_intro',
            method: 'POST',
        } );
        $( '#myplugin-v2-intro' ).fadeOut( 100 );
    } );
} );
```

The `.notice-dismiss` button is generated by core JS when `is-dismissible` is set; bind to that AND any custom dismiss link inside the message.

## Flash notices after a redirect — use a transient

The Settings API handles its own "Settings saved" flash via `settings_errors()` — see **`wp-admin-settings-api`**. For non-Settings-API flows (custom admin form, after a bulk action redirect), use a per-user transient:

```php
// At the end of a successful POST handler:
set_transient( 'myplugin_flash_' . get_current_user_id(), array(
    'message' => __( 'Records imported.', 'myplugin' ),
    'type'    => 'success',
), 60 );
wp_safe_redirect( $back_url );
exit;

// On the next admin page render:
add_action( 'admin_notices', static function (): void {
    $key   = 'myplugin_flash_' . get_current_user_id();
    $flash = get_transient( $key );
    if ( ! is_array( $flash ) || empty( $flash['message'] ) ) {
        return;
    }
    delete_transient( $key );
    $type = sanitize_key( $flash['type'] ?? 'info' );
    if ( ! in_array( $type, array( 'error', 'warning', 'info', 'success' ), true ) ) {
        $type = 'info';
    }

    wp_admin_notice( esc_html( $flash['message'] ), array(
        'type'        => $type,
        'dismissible' => true,
    ) );
} );
```

Per-user keys avoid one user's flash leaking onto another's session.

## Customizing markup — `wp_admin_notice_args` / `wp_admin_notice_markup`

Both filter at `wp-includes/functions.php:9098` and `:9167`. Use the args filter to inject a default class on every notice your plugin emits; use the markup filter to wrap output (e.g. add an icon span).

```php
add_filter( 'wp_admin_notice_args', static function ( array $args, string $message ): array {
    if ( ! empty( $args['id'] ) && str_starts_with( $args['id'], 'myplugin-' ) ) {
        $args['additional_classes'] = array_merge(
            $args['additional_classes'] ?? array(),
            array( 'myplugin-notice' )
        );
    }
    return $args;
}, 10, 2 );
```

## Critical rules

- **Exactly one of the context-specific three fires per page render, plus `all_admin_notices` ALWAYS fires in addition**. Hooking BOTH `admin_notices` and `all_admin_notices` for the same notice produces duplicate output on standard admin pages. Pick one.
- **Always check `get_current_screen()` for plugin-scoped notices**. A bare `add_action( 'admin_notices', ... )` runs on every admin page including unrelated screens.
- **`is-dismissible` only hides client-side — it does NOT persist**. The dismiss button doesn't write anywhere. If you want "dismiss forever", you write the user meta yourself via a REST/AJAX endpoint.
- **Declare `wp-api-fetch` when dismissing through REST**. The admin nonce middleware is attached by that script; otherwise your dismiss call can fail or `wp.apiFetch` can be undefined.
- **The `type` arg must NOT contain spaces**. WP triggers `_doing_it_wrong` and the value still goes directly into `notice-<type>`, producing broken / extra classes.
- **Escape the message yourself**. `wp_admin_notice()` runs `wp_kses_post()` on the final markup — which allows `<a>` / `<strong>` / common HTML but strips dangerous tags. Inline user-controlled data with `esc_html()` / `esc_attr()` / `esc_url()` BEFORE passing to the function. The wrapper is not a sanitizer.
- **`wp_get_admin_notice()` returns raw markup**. It does not run `wp_kses_post()`; only `wp_admin_notice()` does that before echoing.
- **Flash transients must be per-user**. A global `myplugin_flash` key shows User B's "Imported!" notice to every admin on the next page load.
- **Don't render notices in cron / REST contexts**. Hooks like `admin_notices` only fire on admin page renders. A successful background job should write to a transient and let the next admin pageview surface it.

## Common AI mistakes

```php
// WRONG — hand-rolled markup, missing translator-safe wrapper, easy to drift from core CSS
echo '<div class="notice notice-success is-dismissible"><p>Saved!</p></div>';

// RIGHT — let core build the markup
wp_admin_notice( esc_html__( 'Saved!', 'myplugin' ), array( 'type' => 'success', 'dismissible' => true ) );
```

```php
// WRONG — runs on every admin page including unrelated screens
add_action( 'admin_notices', static fn () => wp_admin_notice( '…', array( 'type' => 'warning' ) ) );

// RIGHT — gate on the current screen
add_action( 'admin_notices', static function (): void {
    $s = get_current_screen();
    if ( $s && 'toplevel_page_myplugin' === $s->id ) {
        wp_admin_notice( '…', array( 'type' => 'warning' ) );
    }
} );
```

```php
// WRONG — "is-dismissible" with no persistence; reappears on every reload
wp_admin_notice( '…', array( 'type' => 'info', 'dismissible' => true ) );

// RIGHT — also gate on user meta + REST endpoint to record dismissal (see Persisted dismissal section)
```

```php
// WRONG — global flash key; one user's success leaks to all admins on next load
set_transient( 'myplugin_flash', $msg, 60 );

// RIGHT — scope per user
set_transient( 'myplugin_flash_' . get_current_user_id(), $msg, 60 );
```

## Cross-references

- See **`wp-admin-settings-api`** for `add_settings_error` / `settings_errors` — the Settings-API-native flash mechanism, which is the right tool when the notice belongs to an options page save.
- See **`wp-rest-api`** for the dismiss-notice endpoint shape — permission_callback + capability check.
- See **`wp-admin-list-table`** for `set_transient` flash usage after a bulk action redirect.

## What this skill does NOT cover

- Block editor notices (`wp.data.dispatch( 'core/notices' ).createSuccessNotice`). Different stack — React/Redux state, not the classic PHP banners.
- Network-admin notices via update-core / plugin-install routes. Those have their own machinery (`update-nag`, `core-major-auto-updates-notice`).

## References

- `wp-includes/functions.php:9078` — `wp_get_admin_notice()` definition with full `$args` shape.
- `wp-includes/functions.php:9189` — `wp_admin_notice()` echo helper (runs `wp_kses_post`, fires `wp_admin_notice` action before output).
- `wp-admin/admin-header.php:290-321` — the `if/elseif/else` that picks ONE of `network_admin_notices` (line 299) / `user_admin_notices` (306) / `admin_notices` (313), then unconditional `all_admin_notices` at line 321.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…