Skip to main content

Documentation

No results found.
Features

Popup Rows (page-embedded announcement modals)

WebProCMS lets editors add popup/modal announcements to any page straight from the page builder — promotional offers, seasonal announcements, event reminders, newsletter pitches. A popup is just another design-library row: it's inserted fro...

WebProCMS lets editors add popup/modal announcements to any page straight from the page builder — promotional offers, seasonal announcements, event reminders, newsletter pitches. A popup is just another design-library row: it's inserted from the "Popups" category in the add-row picker, edited with the same sidebar cards as every other row (headings, rich text, buttons, images), and rendered as a full-screen overlay on the live site with configurable timing, frequency capping, and enter/exit animations.


What editors get

  • A "Popups" category in the add-row picker with ready-made templates:
    • Popup - Promo Split — photo on the left, headline + supporting text + CTA button on the right (the classic "we now offer X" announcement).
    • Popup - Image Banner — full-bleed background image behind a dark scrim with centered headline and a CTA pair.
    • Popup - Announcement — simple centered card with an icon badge, headline, rich-text body, and two buttons.
  • Normal row editing. The popup's contents are standard design-library components (x-dl.heading, x-dl.subheadline rich text, x-dl.buttons, x-dl.link, x-dl.image, x-dl.icon), so everything — copy, colors, buttons, images — edits exactly like any other row, including AI text/image generation and translations.
  • Popup behaviour settings on the modal's sidebar card:
    • Enable Popup — master toggle.
    • Show Frequency — Once per day (default), Once ever, or Every visit.
    • Delay (seconds) — how long after page load before the popup opens (default 2s).
    • Enter / Exit Animation — Zoom, Fade, Fade Up, Fade Down, or None. Enter runs at 500ms, exit at a snappier 300ms.
    • Background Image — optional cover-fit image painted behind the panel content.
    • Overlay / Panel / Close Button classes — full Tailwind control over the backdrop scrim, the dialog surface, and the dismiss button.
    • Element ID / Custom Attributes — the standard Advanced fields.
  • In-editor preview. In the page editor and the design-library preview the popup renders statically in-flow (visible panel over its overlay color) so it can be seen and edited without being hijacked by the live trigger logic. On the published page the row occupies no space until it opens.

The Popups rows above open themselves on a timer. To open a row only when a visitor asks for it — a quiz, a booking form, a long explanation — turn the row itself into a popup instead: paintbrush → Advanced → Show As → Popup — opens from a link, and give the row a Section ID (e.g. plan-finder). Then link to it from anywhere on the page with #plan-finder (a button, a link inside rich text, a menu item).

  • Nothing opens it but a link. It has no timer and no frequency cap. Any link to #{id} on the same page opens it, and a link from another page to /that-page#{id} opens it when the page loads. The hash is removed again when the popup closes, so a refresh doesn't reopen it. A link that opens a new tab, downloads, or carries a different query string (?plan=pro#{id}) is a real navigation and is left alone; the page it loads opens the popup from the hash.
  • A link inside the popup to another part of the page (e.g. #contact) closes the popup first, then jumps.
  • Visibility rules still apply. If the section's visibility rule hides it from this visitor, its links do nothing rather than opening an empty popup.
  • It needs a Section ID. Without one nothing could ever open it, so the row simply stays in the page. The sidebar warns when Popup is chosen with no ID.
  • The editor always shows it in the page, with all its sidebar cards, because a popup that's closed in the editor couldn't be edited. Only the live site renders it as a popup.
  • It keeps its look. The row's colours, preset and content come along; its page spacing (py-section, px-section and their variants) is swapped for modal padding, and it gets rounded corners and a shadow. It is as wide as the row's container (e.g. max-w-3xl), never wider than the screen, and scrolls inside itself when it's taller than the screen.
  • Closing: the X button, Escape, or a click on the dimmed backdrop.

How it works. x-dl.section renders the row as a native <dialog data-dl-dialog id="…"> (with a close button) when section_display is popup, the row has a section_id, and the request is not an editor or design-library preview. resources/js/dl-dialog.js (lazy-loaded by public.js when a dialog[data-dl-dialog] exists) opens it with showModal(), which gives the browser's own top layer, backdrop, Escape handling, focus moving in and back out, and an inert page behind it. The script adds only what the browser doesn't: the openers (dialogIdForHref() decides which links count), the X and backdrop clicks, an accessible name from the row's first heading, and a width pinned to the container's max width so a multi-step row doesn't change width between steps. The frame (block layout, fixed position, centering, fit-content height, max size, scrolling, backdrop, page scroll lock) is a hand-written base-layer block in public.css, forced with !important because the row's own section classes sit on the same element (a relative added for textures/dividers would otherwise unfix it, a flex would show it while closed, and a hero's min-h-screen would outgrow the screen). The dialog carries wire:ignore.self so a Livewire round trip on the page can't morph away the open attribute and shut it mid-use. An auto shape divider beside a popup takes its colour from the visible row past it (neighbourRow() in section-dividers.js), not from the closed dialog. Tests: SectionPopupDisplayTest, DlDialogLinkMatchTest (through tests/scripts/run-dl-dialog.mjs).

Frequency capping ("show once per day")

The default behaviour matches the common ad-popup convention: a visitor sees the popup the first time they open that page each day, and not again on subsequent visits to the page that day — closing it or navigating away doesn't bring it back until the visitor's local midnight rolls over.

  • The stamp is stored in the visitor's browser (localStorage), keyed per page path + row instance (dl-popup:{path}:{slug}:{prefix}), so two different popups — or the same popup on two different pages — track independently.
  • "Once per day" uses the visitor's local date, not the server's, so the reset happens at their midnight.
  • "Once ever" shows a single time per browser; "Every visit" shows on every page load (useful while designing, or for must-see notices).
  • The stamp is written when the popup opens (not when it's dismissed), so a reload mid-popup doesn't re-trigger it.
  • Private-browsing visitors with storage blocked simply see the popup each visit — the failure mode is graceful.

Caching architecture (why it's all client-side)

The cached page HTML never varies per visitor — ResponseCache stores one body per URL. The popup therefore ships in every cached response as a hidden fixed-position element carrying its behaviour as data attributes (data-popup-frequency, data-popup-delay, data-popup-enter, data-popup-exit). All per-visitor decisions (has this visitor seen it today? when should it open?) happen in resources/js/dl-popup.js, which the public bundle lazy-imports only when a [data-dl-popup] element is present on the page — pages without popups pay zero JS cost.

Accessibility

  • The live popup renders with role="dialog", aria-modal="true", and an accessible label.
  • Opening moves focus to the panel; closing restores focus to the previously-focused element.
  • Escape closes; clicking the backdrop or the X button closes; Tab focus is contained inside the open dialog.
  • Enter/exit animations are skipped for visitors with prefers-reduced-motion: reduce.
  • Page scroll is locked while the popup is open and always released on close or navigation.

Architecture (for developers)

Piece File Role
Component schema app/Support/DlSchemas/Modal.php Field registration + enterPresets() / exitPresets() (tailwindcss-animate class maps) + frequencyOptions()
Blade component resources/views/components/dl/modal.blade.php Renders the live overlay (hidden + data-attribute contract) or the static preview branch when editor_preview() / design_library_preview()
Runtime resources/js/dl-popup.js Frequency check, delayed open, animations, close paths, focus management, scroll lock
Lazy boot resources/js/public.js Gated on [data-dl-popup]; re-runs on livewire:navigated
Row templates resources/design-library/rows/popups/ The Popups category examples
Category app/Enums/RowCategory.php Popups case (always available, no feature gate)
Tests tests/Feature/DesignLibrary/ModalComponentTest.php Schema, live contract, overrides, toggle-off, preview branch, all three templates

Implementation notes:

  • Row shape. A popup row still uses <x-dl.section> as its outer wrapper (required by the parser/editor), but with empty field-section-classes / field-container-classes — on the live page the section is a zero-height in-flow element containing the fixed overlay.
  • Editor drill-in. x-dl.modal is in RowBladeSurgery::RECURSE_INTO_SLUGS, so its children get their own @dl-item markers and sidebar cards; the modal itself is a card holding the behaviour fields.
  • Structural vs. design classes. Positioning (fixed inset-0 z-[90] hidden items-center justify-center) is hardcoded in the component — it's functional, not design, and swapping it per-branch is what makes the preview render statically. Visual surfaces (overlay scrim, panel, close button) are editable class fields.
  • Animation classes come from Modal::enterPresets() / exitPresets() and are force-included in resources/css/public.css via @source inline(...) (the animate-out family isn't referenced in any scanned file).
  • Select field types popup_frequency and popup_animation are wired in alpineDropdownOptions (editor-state.js) and both type lists in content-field-alpine.blade.php — the same pattern as video_source.
  • No consent gating needed. The popup is first-party markup with no third-party scripts or cookies; localStorage frequency stamps are functional storage, not tracking.