WordPress Admin Notices API: Dismissible, Persistent, and Per-User Notices

WordPress 6.4 formalised the admin notices system with a proper WP_Admin_Notice class and a filter-based API that replaces the old pattern of echoing raw HTML inside admin_notices callbacks. The new API supports notice type, dismissibility, and per-cap targeting, and persisting dismissed state in user meta ensures a notice shown once is never shown again.

Problem: WordPress plugins display admin notices using the admin_notices action, but implementing dismissible notices that stay dismissed per user, notices that appear only on specific screens, and notices with different severity levels requires boilerplate code that each plugin re-implements differently.

Solution: Use a structured Admin Notices class: store dismissed state in user meta with update_user_meta(), check the current screen with get_current_screen() before rendering, send an AJAX request to a dismissal endpoint that sets the meta, and return HTML with a notice-success/warning/error class and the core .notice-dismiss button.


The code below shows the new WP_Admin_Notice usage, an AJAX-based dismiss handler that stores state in user meta, and a helper that checks that state before showing the notice on subsequent page loads.


<?php
// ── 1. Modern notice using WP_Admin_Notice (WordPress 6.4+) ───────────────
add_action( 'admin_notices', function () {
    $user_id    = get_current_user_id();
    $notice_key = 'my_plugin_setup_notice_dismissed';

    // Don't show if already dismissed
    if ( get_user_meta( $user_id, $notice_key, true ) ) {
        return;
    }

    $notice = new WP_Admin_Notice(
        sprintf(
            '<strong>My Plugin</strong> is almost ready. <a href="%s">Complete setup</a>.',
            esc_url( admin_url( 'options-general.php?page=my-plugin' ) )
        ),
        [
            'type'               => 'warning',
            'dismissible'        => true,
            'additional_classes' => [ 'my-plugin-notice' ],
            'attributes'         => [ 'data-notice-key' => $notice_key ],
        ]
    );
    $notice->render();
} );

// ── 2. AJAX handler: mark notice as dismissed in user meta ────────────────
add_action( 'wp_ajax_dismiss_my_plugin_notice', function () {
    check_ajax_referer( 'dismiss_notice_nonce', 'nonce' );
    $key = sanitize_key( $_POST['notice_key'] ?? '' );
    if ( $key ) {
        update_user_meta( get_current_user_id(), $key, '1' );
    }
    wp_send_json_success();
} );

// ── 3. Enqueue the dismiss JS ─────────────────────────────────────────────
add_action( 'admin_enqueue_scripts', function () {
    wp_enqueue_script(
        'my-plugin-notices',
        plugin_dir_url( __FILE__ ) . 'js/notices.js',
        [ 'jquery' ],
        '1.0.0',
        true
    );
    wp_localize_script( 'my-plugin-notices', 'myPluginNotices', [
        'nonce'   => wp_create_nonce( 'dismiss_notice_nonce' ),
        'ajaxUrl' => admin_url( 'admin-ajax.php' ),
    ] );
} );


// js/notices.js
jQuery( document ).on( 'click', '.my-plugin-notice .notice-dismiss', function () {
    const noticeKey = jQuery( this ).closest( '[data-notice-key]' )
                                    .data( 'notice-key' );
    if ( ! noticeKey ) return;

    jQuery.post( myPluginNotices.ajaxUrl, {
        action:     'dismiss_my_plugin_notice',
        nonce:      myPluginNotices.nonce,
        notice_key: noticeKey,
    } );
} );


NOTE: Always sanitise the notice_key value with sanitize_key() before storing it in user meta; an unsanitised key could be used to write arbitrary meta entries if the AJAX endpoint were called directly.