How to Show a Configurable Popup on Frontend Pages in CakePHP 5
A simple CakePHP 5 tutorial to show a popup only on your websiteβs frontend pages. Step-by-step guide with layout setup, active popup logic, and working code. Perfect for beginners.
If you are building a website using CakePHP 5 and want to show a configurable popup message on your frontend pages (but not inside the admin area), then this step-by-step guide is for you.
In this tutorial, you’ll learn how to create a small system that allows an admin to manage popup content (title, message, visibility, etc.) and display it automatically on the frontend pages of your CakePHP 5 site.
The best part — you can easily enable or disable it from the database/admin side setting page without touching the code again.
π― What We Are Building
We’ll create a configurable popup that:
- Shows only on the frontend (public) pages
- Can be managed from the admin panel
- Supports title, message, and delay options
- Loads dynamically based on database values
- Doesn’t disturb the admin area
This type of popup is perfect for showing offers, discounts, or newsletter signup messages on your website.
π§± Step 1: Create the Popups Table
First, you need a table in your database to store popup content. This table will hold your popup title, content, active flag, delay (in milliseconds), and timestamps.You can create it manually or through a migration. Below is a simple SQL structure for the popups table:
CREATE TABLE `popups` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(255) NOT NULL,
`content` text NOT NULL,
`show_once` tinyint(1) NOT NULL DEFAULT 0,
`delay_ms` int(11) NOT NULL DEFAULT 3000,
`active` tinyint(1) NOT NULL DEFAULT 1,
`created` datetime DEFAULT current_timestamp(),
`modified` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Example data:
INSERT INTO `popups`
(`title`, `content`, `show_once`, `delay_ms`, `active`) VALUES
('Welcome to our site!', 'Subscribe to our newsletter and get 10% off your first order.', 0, 2000, 1);
We’ll create a table file for Popups.
src/Model/Table/PopupsTable.php
<?php
declare(strict_types=1);
namespace App\Model\Table;
use Cake\ORM\Table;
class PopupsTable extends Table
{
public function initialize(array $config): void
{
parent::initialize($config);
$this->setTable('popups'); // database table name
$this->setDisplayField('title'); // default display field
$this->setPrimaryKey('id'); // primary key
$this->addBehavior('Timestamp'); // adds created/modified fields
}
}
βοΈ Step 2. Routes Configuration
We’ll define separate routes for the frontend and admin panels. This helps us load different layouts for each area.
<?php
use Cake\Routing\RouteBuilder;
use Cake\Routing\Route\DashedRoute;
return function (RouteBuilder $routes): void {
$routes->setRouteClass(DashedRoute::class);
// Frontend routes
$routes->scope('/', function (RouteBuilder $builder): void {
$builder->connect('/', ['controller' => 'Home', 'action' => 'index']);
$builder->fallbacks(DashedRoute::class);
});
// Admin routes
$routes->prefix('Admin', function (RouteBuilder $builder): void {
$builder->connect('/popup', ['controller' => 'Popups', 'action' => 'edit']);
$builder->fallbacks(DashedRoute::class);
});
};
π Here, the / route shows our frontend home page, while /admin/popup is used to edit the popup.
π§ Step 3. Admin Controller Setup
We’ll create a base Admin controller and one for managing the popup.
src/Controller/Admin/AppController.php
<?php
declare(strict_types=1);
namespace App\Controller\Admin;
use App\Controller\AppController as BaseController;
class AppController extends BaseController
{
public function initialize(): void
{
parent::initialize();
$this->viewBuilder()->setLayout('admin');
}
}
src/Controller/Admin/PopupsController.php
The PopupsController inside the Admin namespace manages the backend popup configuration. It provides an interface for admins to update popup content, delay time, and visibility status.
<?php
declare(strict_types=1);
namespace App\Controller\Admin;
class PopupsController extends AppController
{
public function edit()
{
$popup = $this->Popups->find()->first() ?? $this->Popups->newEmptyEntity();
if ($this->request->is(['post', 'put', 'patch'])) {
$popup = $this->Popups->patchEntity($popup, $this->request->getData());
if ($this->Popups->save($popup)) {
$this->Flash->success('Popup saved successfully.');
} else {
$this->Flash->error('Unable to save popup.');
}
}
$this->set(compact('popup'));
}
}
π¨ Step 4. Admin Layout (No Popup Here)
We’ll use a simple admin layout to manage backend pages. This ensures our popup is not loaded in admin routes.
templates/layout/admin.php
<!DOCTYPE html>
<html>
<head>
<?= $this->Html->charset() ?>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title> <?= $cakeDescription ?>: <?= $this->fetch('title') ?> </title>
<?= $this->Html->meta('icon') ?> <?= $this->Html->css(['normalize.min', 'milligram.min', 'fonts', 'cake']) ?> <?= $this->fetch('meta') ?> <?= $this->fetch('css') ?> <?= $this->fetch('script') ?>
</head>
<body>
<nav class="top-nav">
<div class="top-nav-title"> <a href="<?= $this->Url->build('/') ?>"><span>Cake</span>PHP</a> </div>
<div class="top-nav-links"> <a target="_blank" rel="noopener" href="https://book.cakephp.org/5/">Documentation</a> <a target="_blank" rel="noopener" href="https://api.cakephp.org/">API</a> </div>
</nav>
<main class="main">
<div class="container"> <?= $this->Flash->render() ?> <?= $this->fetch('content') ?> </div>
</main>
<footer> </footer>
</body>
</html>
templates/Admin/Popups/edit.php
<div class="popup-settings content">
<h2>Edit Popup Settings</h2>
<?= $this->Form->create($popup) ?>
<fieldset>
<legend>Popup Configuration</legend>
<?= $this->Form->control('title') ?>
<?= $this->Form->control('content', ['type' => 'textarea', 'rows' => 4]) ?>
<?= $this->Form->control('show_once', ['type' => 'checkbox', 'label' => 'Show only once per session']) ?>
<?= $this->Form->control('delay_ms', ['label' => 'Delay (milliseconds)']) ?>
<?= $this->Form->control('active', ['type' => 'checkbox', 'label' => 'Activate popup']) ?>
</fieldset>
<?= $this->Form->button(__('Save Changes')) ?>
<?= $this->Form->end() ?>
</div>
π‘ Step 5. Frontend Layout (Popup Displayed)
templates/layout/default.php
Now, we will include our popup element in the default frontend layout so it appears only on normal website pages — not inside the admin.
<!DOCTYPE html>
<html>
<head>
<title><?= $this->fetch('title') ?></title>
<?= $this->Html->css(['milligram.min', 'cake', 'popup']) ?>
</head>
<body>
<main>
<?= $this->fetch('content') ?>
</main>
<!-- popup display code START -->
<?php
use Cake\ORM\TableRegistry;
$popupsTable = TableRegistry::getTableLocator()->get('Popups');
$popup = $popupsTable->find()->where(['active' => 1])->first();
if ($popup):
echo $this->element('popup', ['popup' => $popup]);
endif;
?>
<?= $this->Html->css('popup') ?>
<?= $this->Html->script('popup') ?>
<!-- popup display code END -->
</body>
</html>
πͺ Step 6. Popup Element
In CakePHP, elements are reusable pieces of code that can be included in layout or templates.
templates/element/popup.php
<?php
$showOnce = (bool)$popup->show_once;
$delayMs = (int)$popup->delay_ms;
$storageKey = 'cake_popup_seen_v1'; // bump this if you want to force show again
?>
<!-- Popup markup -->
<div id="global-welcome-popup" class="cpopup-overlay" aria-hidden="true" style="display:none;">
<div class="cpopup" role="dialog" aria-modal="true" aria-labelledby="cpopup-title">
<button class="cpopup-close" aria-label="Close">×</button>
<div id="cpopup-title" class="cpopup-title"><?= h($popup->title) ?></div>
<div class="cpopup-content"><?= $this->Text->autoParagraph(h($popup->content)) ?></div>
<div class="cpopup-footer">
<button class="cpopup-ok">OK</button>
</div>
</div>
</div>
<script>
window.__CPUP = {
showOnce: <?= $showOnce ? 'true' : 'false' ?>,
delay: <?= $delayMs ?>,
storageKey: <?= json_encode($storageKey) ?>
};
</script>
π¨ Step 7. CSS (popup.css)
The popup.css file defines the styles for how the popup looks on the frontend. It handles layout, colors, overlay background, spacing, and animations — ensuring the popup appears centered, readable, and visually appealing to users.
/* webroot/css/popup.css */
.cpopup-overlay {
position: fixed;
inset: 0;
display: none;
align-items: center;
justify-content: center;
background: rgba(0,0,0,0.5);
z-index: 9999;
padding: 20px;
box-sizing: border-box;
}
.cpopup {
background: #fff;
border-radius: 8px;
max-width: 600px;
width: 100%;
padding: 18px;
box-shadow: 0 10px 30px rgba(0,0,0,0.25);
position: relative;
animation: cpFadeIn 0.2s ease-out;
}
.cpopup-close {
position: absolute;
right: 12px;
top: 10px;
border: none;
background: transparent;
font-size: 22px;
cursor: pointer;
}
.cpopup-title {
font-size: 20px;
font-weight: 600;
margin-bottom: 10px;
}
.cpopup-content {
margin-bottom: 16px;
line-height: 1.5;
}
.cpopup-footer {
text-align: right;
}
.cpopup-ok {
padding: 8px 14px;
border-radius: 4px;
border: 0;
background: #007bff;
color: #fff;
cursor: pointer;
}
@keyframes cpFadeIn {
from { opacity: 0; transform: translateY(-8px) }
to { opacity: 1; transform: translateY(0) }
}
/* small-screen tweaks */
@media (max-width: 480px) {
.cpopup { padding: 14px; }
.cpopup-title { font-size: 18px; }
}
βοΈ Step 8. JavaScript (popup.js)
The popup.js file controls how and when the popup appears on the screen. It checks the delay, handles the “show once” logic using local storage, and manages open/close actions when users click buttons — making the popup interactive and user-friendly.
// webroot/js/popup.js
document.addEventListener('DOMContentLoaded', function () {
try {
var cfg = window.__CPUP;
if (!cfg) return;
var key = cfg.storageKey || 'cake_popup_seen_v1';
var showOnce = !!cfg.showOnce;
var delay = Number(cfg.delay) || 3000;
// If set to show once and already seen, do nothing
if (showOnce && window.localStorage && localStorage.getItem(key)) {
return;
}
// Find popup element (from element/popup.php)
var popupOverlay = document.getElementById('global-welcome-popup');
if (!popupOverlay) return;
var popupDialog = popupOverlay.querySelector('.cpopup');
var closeBtn = popupOverlay.querySelector('.cpopup-close');
var okBtn = popupOverlay.querySelector('.cpopup-ok');
function showPopup() {
popupOverlay.style.display = 'flex';
popupOverlay.setAttribute('aria-hidden', 'false');
}
function hidePopup() {
popupOverlay.style.display = 'none';
popupOverlay.setAttribute('aria-hidden', 'true');
if (showOnce && window.localStorage) {
localStorage.setItem(key, '1');
}
}
if (closeBtn) closeBtn.addEventListener('click', hidePopup);
if (okBtn) okBtn.addEventListener('click', hidePopup);
// clicking on overlay (outside dialog) closes
popupOverlay.addEventListener('click', function (e) {
if (e.target === popupOverlay) hidePopup();
});
// show after configured delay
setTimeout(showPopup, delay);
} catch (err) {
console.error('Popup script error', err);
}
});
π Step 9. Frontend Home Page
src/Controller/HomeController.php
<?php
declare(strict_types=1);
namespace App\Controller;
class HomeController extends AppController
{
public function index()
{
$this->viewBuilder()->setLayout('default');
}
}
templates/Home/index.php
<h1>Welcome to Home Page</h1>
<p>This is your frontend page where popup will automatically appear.</p>
π§± Folder Structure
Your structure will look like this:
src/
βββ Controller/
β βββ AppController.php
β βββ HomeController.php
β βββ Admin/
β βββ AppController.php
β βββ PopupsController.php
βββ Model/
β βββ Table/PopupsTable.php
templates/
βββ Home/index.php
βββ Admin/
β βββ Popups/edit.php
βββ element/popup.php
βββ layout/
βββ default.php
βββ admin.php
config/
βββ routes.php
webroot/
βββ js/popup.js
βββ css/popup.css
β Final Result
- / → Home page (popup appears after delay)
- /admin/popup → Admin panel for editing popup content (no popup shown here)
Now you have a fully working popup system — simple, reusable, and easy to extend!
π¬ Final Thoughts
This approach gives you a clean structure:
- Frontend layout loads popup automatically.
- Admin layout stays clean without it.
- Popup content is managed from the database.
- JavaScript ensures it respects your “show once” and delay settings.
You can later extend it for multiple popup types, scheduling, or different designs. Even if you’re a beginner in CakePHP, this example gives you a good foundation for mixing backend logic with a simple frontend feature.
Happy coding! π°