How to Add Custom Admin Notices (Dismissible)

December 22, 2025
How to Add Custom Admin Notices (Dismissible)

How to Add Custom Admin Notices (Dismissible)

Admin notices are one of the simplest ways to communicate important messages to editors and admins:
content guidelines, migration warnings, required actions, or environment banners (staging vs production).

This article shows a production-safe approach to creating dismissible custom admin notices
without relying on plugins, including:

  • Showing notices only on specific admin screens
  • Dismissing per-user (stored in user meta)
  • Adding an optional “Remind me later” behavior (stored with expiration)
  • Nonce-protected dismissal to prevent abuse

Why Use User Meta for Dismissal

WordPress admin is shared by multiple users. A global option would dismiss the notice for everyone,
which is rarely what you want. Using user_meta makes dismissal per-user and scalable.

Recommended Implementation

The following code provides a reusable system:

  • Register one or more notices with unique IDs
  • Render them via admin_notices
  • Dismiss via a nonce-protected URL (no JavaScript required)

1) Add the Notice Renderer

<?php
/**
 * Dismissible admin notices (per-user).
 *
 * Drop this into functions.php or a small mu-plugin.
 */

add_action( 'admin_notices', function () {
  if ( ! is_user_logged_in() ) {
    return;
  }

  // Only show to users who can manage the site (adjust as needed).
  if ( ! current_user_can( 'manage_options' ) ) {
    return;
  }

  $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
  $screen_id = $screen ? (string) $screen->id : '';

  // Example: show only on Dashboard + Pages list.
  $allowed_screens = array( 'dashboard', 'edit-page' );
  if ( $screen_id && ! in_array( $screen_id, $allowed_screens, true ) ) {
    return;
  }

  $notices = wpct_get_admin_notices();

  foreach ( $notices as $notice ) {
    wpct_render_admin_notice( $notice );
  }
} );

/**
 * Define notices in one place.
 *
 * Each notice must have a stable unique ID.
 */
function wpct_get_admin_notices(): array {
  return array(
    array(
      'id'      => 'wpct_env_banner',
      'type'    => 'warning', // success|info|warning|error
      'message' => 'You are currently viewing the STAGING environment. Do not publish urgent content here.',
      'cap'     => 'manage_options',
      'mode'    => 'dismiss', // dismiss|remind (optional behavior)
      'remind_days' => 3,      // only used when mode=remind
    ),
    array(
      'id'      => 'wpct_editor_guideline',
      'type'    => 'info',
      'message' => 'Reminder: Use the Featured Image at 1280×720 and avoid uploading uncompressed PNGs.',
      'cap'     => 'edit_pages',
      'mode'    => 'dismiss',
    ),
  );
}

/**
 * Render a single notice if not dismissed (or if reminder expired).
 */
function wpct_render_admin_notice( array $notice ): void {
  $user_id = get_current_user_id();

  $id      = isset( $notice['id'] ) ? (string) $notice['id'] : '';
  $type    = isset( $notice['type'] ) ? (string) $notice['type'] : 'info';
  $message = isset( $notice['message'] ) ? (string) $notice['message'] : '';
  $cap     = isset( $notice['cap'] ) ? (string) $notice['cap'] : '';

  if ( $id === '' || $message === '' ) {
    return;
  }

  if ( $cap && ! current_user_can( $cap ) ) {
    return;
  }

  // Dismiss state stored per user.
  $meta_key_dismissed = 'wpct_notice_dismissed_' . $id;
  $meta_key_until     = 'wpct_notice_until_' . $id; // for "remind me later"

  $dismissed = (string) get_user_meta( $user_id, $meta_key_dismissed, true );
  if ( $dismissed === '1' ) {
    return;
  }

  // Optional: reminder expiration (if set, hide until timestamp).
  $until = (int) get_user_meta( $user_id, $meta_key_until, true );
  if ( $until > time() ) {
    return;
  }

  // Build dismissal URL (nonce protected).
  $dismiss_url = wp_nonce_url(
    add_query_arg(
      array(
        'wpct_notice_action' => 'dismiss',
        'wpct_notice_id'     => $id,
      ),
      admin_url()
    ),
    'wpct_notice_' . $id
  );

  // Optional "remind me later" URL.
  $remind_url = '';
  if ( isset( $notice['mode'] ) && $notice['mode'] === 'remind' ) {
    $remind_url = wp_nonce_url(
      add_query_arg(
        array(
          'wpct_notice_action' => 'remind',
          'wpct_notice_id'     => $id,
        ),
        admin_url()
      ),
      'wpct_notice_' . $id
    );
  }

  $allowed_types = array( 'success', 'info', 'warning', 'error' );
  if ( ! in_array( $type, $allowed_types, true ) ) {
    $type = 'info';
  }

  echo '<div class="notice notice-' . esc_attr( $type ) . ' is-dismissible">';
  echo '<p>' . esc_html( $message ) . '</p>';
  echo '<p>';
  echo '<a href="' . esc_url( $dismiss_url ) . '">' . esc_html__( 'Dismiss', 'default' ) . '</a>';
  if ( $remind_url ) {
    echo ' | <a href="' . esc_url( $remind_url ) . '">' . esc_html__( 'Remind me later', 'default' ) . '</a>';
  }
  echo '</p>';
  echo '</div>';
}

2) Handle Dismiss / Remind Actions Securely

Now add a handler that runs in admin to store dismissal state in user meta.

<?php
add_action( 'admin_init', function () {
  if ( ! is_user_logged_in() ) {
    return;
  }

  $action = isset( $_GET['wpct_notice_action'] ) ? sanitize_key( (string) $_GET['wpct_notice_action'] ) : '';
  $id     = isset( $_GET['wpct_notice_id'] ) ? sanitize_key( (string) $_GET['wpct_notice_id'] ) : '';

  if ( $action === '' || $id === '' ) {
    return;
  }

  // Validate nonce
  if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( (string) $_GET['_wpnonce'], 'wpct_notice_' . $id ) ) {
    return;
  }

  $user_id = get_current_user_id();

  // Optional capability gate (keep it simple: only logged-in users can dismiss their own notices).
  if ( ! current_user_can( 'read' ) ) {
    return;
  }

  $meta_key_dismissed = 'wpct_notice_dismissed_' . $id;
  $meta_key_until     = 'wpct_notice_until_' . $id;

  if ( $action === 'dismiss' ) {
    update_user_meta( $user_id, $meta_key_dismissed, '1' );
    delete_user_meta( $user_id, $meta_key_until );
  }

  if ( $action === 'remind' ) {
    // Default: hide for 3 days if not configured elsewhere.
    $days  = 3;
    $until = time() + ( $days * DAY_IN_SECONDS );
    update_user_meta( $user_id, $meta_key_until, (string) $until );
  }

  // Redirect back to remove query args (prevents repeated actions on refresh).
  wp_safe_redirect( remove_query_arg( array( 'wpct_notice_action', 'wpct_notice_id', '_wpnonce' ) ) );
  exit;
} );

Make “Remind Me Later” Respect Per-Notice Days

The sample uses a default of 3 days. If you want per-notice configuration (recommended),
store the days in the notice definition and read it inside the handler.
A simple approach is to look up the notice by ID.

<?php
function wpct_get_notice_by_id( string $id ): array {
  foreach ( wpct_get_admin_notices() as $notice ) {
    if ( isset( $notice['id'] ) && (string) $notice['id'] === $id ) {
      return $notice;
    }
  }
  return array();
}
<?php
// Replace the "remind" block in admin_init handler with this:
if ( $action === 'remind' ) {
  $notice = wpct_get_notice_by_id( $id );

  $days = 3;
  if ( isset( $notice['remind_days'] ) && is_numeric( $notice['remind_days'] ) ) {
    $days = max( 1, (int) $notice['remind_days'] );
  }

  $until = time() + ( $days * DAY_IN_SECONDS );
  update_user_meta( $user_id, $meta_key_until, (string) $until );
}

Targeting Specific Admin Screens

If you need more precise targeting, check the current screen ID and only render the notice where it matters.
Common screen IDs include:

  • dashboard
  • edit-post (Posts list)
  • edit-page (Pages list)
  • post (Post edit)
  • page (Page edit)

This reduces noise and increases the chance the notice is actually read.

Common Mistakes to Avoid

  • Storing dismissal globally in options (dismisses for everyone)
  • Skipping nonce checks (allows forced dismissal via crafted URLs)
  • Showing notices on every screen (notice blindness)
  • Relying on JavaScript-only dismissal (breaks with strict admin environments)

Advanced Tip: Multiple Notices Without Clutter

If you plan to add many notices, treat them as “messages” with:

  • Stable IDs
  • Clear capabilities
  • Screen targeting
  • Expiry or reminder logic

This keeps the admin usable and avoids training users to ignore notices.

Summary

  • Use admin_notices to render and admin_init to process dismissal actions
  • Store dismissal per-user via user_meta
  • Protect actions with nonces
  • Target specific screens and capabilities
  • Optionally add “Remind me later” with an expiration timestamp
Avatar

Written by

satoshi

I’ve been building and customizing WordPress themes for over 10 years. In my free time, you’ll probably find me enjoying a good football match.