How to Add a Pop-Up Banner for a Specific WordPress Post (No Plugins)

Nick Cesarz
Nick Cesarz
Milwaukee-area Wordpress web designer, BricksBuilder & EtchWP user, musician and producer in Vinyl Theatre.
Add a Pop Up Modal To Your Wordpress Site
BG

If you want a simple pop-up banner on your WordPress site that promotes a specific post, without installing a plugin, this approach gets the job done with a single PHP snippet.

This method:

  • Pulls title, excerpt, featured image, and link from a post ID
  • Uses inline CSS and JavaScript
  • Loads only on the front end
  • Requires no plugin (works in WPCodeBox, Code Snippets, or functions.php)

Below is a complete working example, followed by instructions on how to use and customize it.

How it works

This snippet creates a modal-style pop-up that appears after a short delay and pulls its content directly from a specific WordPress post.

The pop-up displays the post’s featured image (or a simple placeholder if no image is set), along with the post title and excerpt, and includes a clear link that takes users to the full post.

Visitors can dismiss the pop-up at any time, and it can be configured to appear only once per browser session if desired.

This approach is useful for promoting a new article, highlighting important or evergreen content, or drawing attention to announcements, updates, or other posts you want visitors to notice without relying on additional plugins.

How to use the snippet

Step 1: add the code

Paste the entire snippet below into one of the following:

  • WPCodeBox (PHP snippet, frontend)
  • Code Snippets plugin (frontend only)
  • Your theme’s functions.php file

⚠️ If using functions.php, always test on a staging site first.

Step 2: set the post ID

At the top of the snippet, update this line:

$post_id = 3185; // change this to your post ID

This is the post that will appear in the pop-up. You can find the post ID by going to the WordPress Dashboard -> Posts -> and hovering over a URL to the post and you’ll see the ID in the link URL.

Step 3: adjust timing (optional)

$delay_ms = 2500; // delay in milliseconds

  • 2500 = 2.5 seconds
  • Increase if you want it to appear later
  • Decrease for faster visibility

Step 4: control how often it appears

$show_once = true;

  • true → shows once per browser session
  • false → shows every page load

This uses sessionStorage, so it resets when the browser is closed.

The full code snippet

<?php
/**
 * Sitewide Post Promo Modal (single-snippet: inline CSS + JS)
 * Front-end only (prints in wp_footer).
 */

add_action('wp_footer', function () {

  // ==== do not run in admin/editor/REST/AJAX/etc ====
  if (is_admin()) return;
  if (wp_doing_ajax()) return;
  if (defined('REST_REQUEST') && REST_REQUEST) return;
  if (wp_is_json_request()) return; // blocks JSON endpoints used by editor
  if (is_feed() || is_embed()) return;

  // Block wp-login and similar
  $pagenow = $GLOBALS['pagenow'] ?? '';
  if ($pagenow === 'wp-login.php') return;

  // Block previews
  if (is_preview()) return;

  // ====== CONFIG ======
  $post_id   = 3185;  // Change this to your post ID
  $delay_ms  = 2500;  // Popup delay (milliseconds)
  $show_once = true;  // Show only once per session
  // ====================

  $post = get_post($post_id);
  if (!$post || $post->post_status !== 'publish') return;

  // Build post data
  $title   = get_the_title($post);
  $link    = get_permalink($post);
  $excerpt = has_excerpt($post)
    ? get_the_excerpt($post)
    : wp_trim_words(wp_strip_all_tags($post->post_content), 26, '…');

  // Featured image
  $img_html = '';
  if (has_post_thumbnail($post)) {
    $img_html = get_the_post_thumbnail(
      $post,
      'large',
      [
        'class'    => 'wppm__img',
        'loading'  => 'lazy',
        'decoding' => 'async',
        'alt'      => esc_attr($title),
      ]
    );
  }

  // Unique IDs
  $modal_id   = 'wppm-' . (int) $post_id;
  $overlay_id = $modal_id . '-overlay';
  $dialog_id  = $modal_id . '-dialog';

  ?>
  <style>
    /* ===== WP Post Modal ===== */
    #<?php echo esc_attr($overlay_id); ?>{
      position: fixed;
      inset: 0;
      background: rgba(0,0,0,.55);
      display: none;
      align-items: center;
      justify-content: center;
      padding: 18px;
      z-index: 999999;
    }
    #<?php echo esc_attr($overlay_id); ?>[data-open="1"]{ display:flex; }

    #<?php echo esc_attr($dialog_id); ?>{
      width: 100%;
      max-width: 1040px;
      background: #111;
      color: #fff;
      border-radius: 3px;
      overflow: hidden;
      box-shadow: 0 20px 60px rgba(0,0,0,.45);
      border: 1px solid rgba(255,255,255,.08);
      transform: translateY(8px);
      opacity: 0;
      transition: opacity .18s ease, transform .18s ease;
    }
    #<?php echo esc_attr($overlay_id); ?>[data-open="1"] #<?php echo esc_attr($dialog_id); ?>{
      transform: translateY(0);
      opacity: 1;
    }

    .wppm__wrap{ position: relative; width:100%; max-width:1040px; }
    .wppm__close{
      position:absolute; top:10px; right:10px;
      width:40px; height:40px; border-radius:999px;
      border:1px solid rgba(255,255,255,.14);
      background: rgba(0,0,0,.35);
      color:#fff; display:inline-flex; align-items:center; justify-content:center;
      cursor:pointer;
    }
    .wppm__close:hover{ background: rgba(0,0,0,.5); }

    .wppm__grid{ display:grid; grid-template-rows: 2fr; gap:0; }
    .wppm__media{
      position:relative;
      width:100%;
      aspect-ratio: 2038 / 680;
      background:#0c0c0c;
      overflow:hidden;
    }
    .wppm__img{ width:100%; height:100%; object-fit:cover; display:block; }

    .wppm__body{ padding: 22px 22px 20px; }
    .wppm__title{ margin:0 0 10px; font-size:22px; line-height:1.2; }
    .wppm__excerpt{ margin:0 0 16px; font-size:15px; line-height:1.55; color:rgba(255,255,255,.82); }

    .wppm__actions{ display:flex; gap:10px; flex-wrap:wrap; align-items:center; }
    .wppm__btn{
      display:inline-flex; align-items:center; justify-content:center;
      padding:11px 14px; border-radius:3px;
      border:1px solid rgba(255,255,255,.14);
      background: rgba(255,255,255,.06);
      color:#fff; text-decoration:none; font-weight:600; font-size:14px;
      cursor:pointer;
    }
    .wppm__btn:hover{ background: rgba(255,255,255,.10); }
    .wppm__btn--primary{ background:#fff; color:#111; border-color: rgba(255,255,255,.6); }

    @media (max-width: 820px){
      .wppm__media{ min-height:200px; }
      .wppm__body{ padding:18px; }
      .wppm__title{ font-size:20px; }
    }
  </style>

  <div id="<?php echo esc_attr($overlay_id); ?>" role="presentation" aria-hidden="true">
    <div class="wppm__wrap">
      <button class="wppm__close" type="button" aria-label="Close dialog" data-wppm-close="1">✕</button>

      <div
        id="<?php echo esc_attr($dialog_id); ?>"
        role="dialog"
        aria-modal="true"
        aria-labelledby="<?php echo esc_attr($modal_id); ?>-title"
      >
        <div class="wppm__grid">
          <div class="wppm__media">
            <?php
              if ($img_html) {
                echo $img_html; // WP-generated
              } else {
                echo '<div style="width:100%;height:100%;min-height:220px;background:linear-gradient(135deg, rgba(255,255,255,.08), rgba(255,255,255,.02));"></div>';
              }
            ?>
          </div>

          <div class="wppm__body">
            <h2 class="wppm__title" id="<?php echo esc_attr($modal_id); ?>-title"><?php echo esc_html($title); ?></h2>
            <p class="wppm__excerpt"><?php echo esc_html($excerpt); ?></p>

            <div class="wppm__actions">
              <a class="wppm__btn wppm__btn--primary" href="<?php echo esc_url($link); ?>">Read More</a>
              <button class="wppm__btn" type="button" data-wppm-close="1">Not Now</button>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>

  <script>
  (function(){
    var overlayId  = <?php echo json_encode($overlay_id); ?>;
    var delayMs    = <?php echo (int) $delay_ms; ?>;
    var showOnce   = <?php echo $show_once ? 'true' : 'false'; ?>;
    var sessionKey = "wppm_seen_" + <?php echo json_encode((string)$post_id); ?>;

    // Extra JS-side guard: if we're somehow inside wp-admin/editor, bail.
    if (document.body && document.body.classList && document.body.classList.contains('wp-admin')) return;

    var overlay = document.getElementById(overlayId);
    if (!overlay) return;

    function openModal(){
      if (showOnce && sessionStorage.getItem(sessionKey) === "1") return;
      overlay.setAttribute("data-open","1");
      overlay.setAttribute("aria-hidden","false");
      document.documentElement.style.overflow = "hidden";
      if (showOnce) sessionStorage.setItem(sessionKey, "1");
    }

    function closeModal(){
      overlay.removeAttribute("data-open");
      overlay.setAttribute("aria-hidden","true");
      document.documentElement.style.overflow = "";
    }

    overlay.addEventListener("click", function(e){
      if (e.target === overlay) closeModal();
      var t = e.target;
      if (t && t.getAttribute && t.getAttribute("data-wppm-close") === "1") closeModal();
    });

    document.addEventListener("keydown", function(e){
      if (e.key === "Escape" && overlay.getAttribute("data-open") === "1") closeModal();
    });

    window.setTimeout(openModal, delayMs);
  })();
  </script>
  <?php

}, 100);

Why it’s safe to use in most cases

This snippet safely escapes all output using WordPress functions like esc_htmlesc_url, and esc_attr to prevent unwanted code from being rendered on the page. It only loads content from posts that are published, so drafts or private posts are never exposed.

The code does not process any form submissions and does not rely on AJAX or perform any database writes, which reduces both complexity and risk. It also avoids exposing any admin-only data or functionality on the front end.

As long as you’re using it to promote content you control on your own site, this makes it a low-risk and lightweight solution.

However, I can’t foresee every scenario, so make sure you test this on a staging environment before deploying live.

Customization ideas

Once it’s working, you can easily:

  • Change the button text
  • Add a second CTA
  • Restrict it to certain pages
  • Hide it on the promoted post itself
  • Replace sessionStorage with cookies for longer persistence

I suggest changing out the CSS with your theme’s tokens. You can use the inspect tool to find CSS variables that your theme already uses sitewide, like var(--primary).

When you should use a plugin instead

Use a plugin if you need:

  • Visual editors
  • A/B testing
  • Analytics tracking
  • Multiple pop-ups with targeting rules

For a single, controlled promo, this snippet is often faster and cleaner.

So there ya go, hopefully this helps in your next project!