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.phpfile
⚠️ 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 sessionfalse→ 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_html, esc_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
sessionStoragewith 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!

