Popup system
How PopupMonitor coordinates Klaviyo forms and the theme's own popups, including timing, priority, the per-page limit and render-time hiding.
The problem it solves
Klaviyo decides for itself when to show a form. The theme has its own popups. Neither knows about the other, so without coordination a shopper can get two overlapping modals, or a form the instant the page loads.
PopupMonitor puts one object in charge. Every popup registers with the monitor, and the monitor decides which one
opens and when.
The parts
| Part | File | Responsibility |
|---|---|---|
PopupMonitor | components/popup-logic/index.ts | Holds every registered popup, enforces priority and the per-page limit |
Popup | components/popup-logic/popup.ts | One popup, with an ID, a display name and a priority |
| Adapters | components/popup-logic/adapters/ | Show and hide one kind of popup |
| Sniffers | components/popup-logic/sniffers/ | Watch the DOM for popups that appear after page load |
PopupTimer | components/popup-logic/timer.ts | Fires a callback every 15 seconds |
| Definitions | components/popup-logic/definitions/ | One file per popup, describing when it may show |
Adapters exist because a Klaviyo form and a theme popup are shown and hidden in different ways. Every adapter has the
same methods, show, hide, isVisible, getId and getForceShow, so the monitor does not need to know which kind
of popup it holds.
Sniffers exist because Klaviyo adds its forms to the DOM at a time the theme does not control. The Klaviyo sniffer
watches for elements matching [data-testid^="klaviyo-form-"] and registers each new form with the monitor.
When a popup opens
src/entrypoints/popup-logic.ts sets up the monitor on DOMContentLoaded and makes two kinds of attempt.
The first attempt runs 5 seconds after setup. It asks the monitor for the next popup that has not been shown and tries to open it.
After that, PopupTimer fires every 15 seconds and makes the same request. The timer counts with
requestAnimationFrame, so it pauses while the tab is in the background. Closing any popup resets the count to zero, so
the shopper always gets the full 15 seconds between popups.
If a popup fails to open, the entrypoint removes it from the monitor, and it cannot open again on that page view. The
Klaviyo adapter fails to open a form that the shopper has already closed, according to the klaviyoOnsite entry in
localStorage.
The rules the monitor enforces
canShowPopup returns true only when fewer popups have opened than the per-page limit and no popup is open now.
The limit is maxPerPage, which defaults to two. The entrypoint constructs the monitor without that option, so the
limit is always two. snippets/popup-logic.liquid writes a product's custom.max_allowed_popups metafield to
window.theme.popupManagerConfig.maxAllowedPopups, and only when the blacklist has entries. No script reads that value.
getNextPopup returns the first registered popup that has not been shown yet. The monitor keeps popups sorted by
priority, highest first. A popup that is not chosen stays in the list and can open on a later timer tick, if the limit
allows.
The four registered definitions use the default priority of 0. The sort keeps equal priorities in registration order, so they open in the order they register. The Klaviyo sniffer gives each detected form a negative priority equal to the number of popups already registered, so detected Klaviyo forms rank below the definitions, in the order they appeared.
A popup whose adapter sets forceShow skips canShowPopup, so it ignores both the limit and the rule that nothing else
is open. If another popup is open, the forced popup replaces it only when its priority is higher. Otherwise it does not
open.
Hiding at render time
snippets/popup-logic.liquid reads the Blacklist theme setting and writes a <style> tag for each entry when the
page is rendered. Each tag hides its popup with display: none before any script runs, so the popup does not flash on
screen.
Each blacklist line is a selector and a type, separated by a comma. For a klaviyo entry, the snippet builds a selector
from the form ID that targets Klaviyo's rendered form. For a custom entry, it uses the text as a CSS selector. The
style tag's ID is popup-logic-style- followed by the selector with any . and # removed.
When the monitor opens a popup, clearSSRStyle removes the style tag whose ID matches that popup's ID. No script reads
the blacklist itself. If Klaviyo injects a blacklisted form, the sniffer registers it like any other form. When the
monitor later opens it, clearSSRStyle removes the hiding style and the form appears. The blacklist is therefore
reliable only for popups that never register with the monitor.
Which popups are registered
src/entrypoints/popup-logic.ts registers four definitions:
| Definition | Scope |
|---|---|
sign-up-forms | Everywhere except Medispa pages |
sign-up-forms-medispa | Medispa pages only |
select-brands | A whitelist of brand product URLs |
need-help-popup | Pages that include the section |
Three more definition files are not registered: global-popup, cash-vouchers and street-sale. The global-popup
import is commented out in the entrypoint, and nothing imports the other two. See
Legacy and unused code.
Store staff should know this. The theme editor still shows settings for the global popup, and editing them changes nothing.
Popups outside the definitions
src/entrypoints/klaviyo-popup-trigger.ts runs the offers link on product and Medispa pages. It returns early unless
window.offerPopupConfig has both a desktop and a mobile form ID. On click, it picks the desktop ID when
window.outerWidth is above 768 pixels and the mobile ID otherwise, creates a Popup with forceShow set, adds it to
the monitor and opens it. It creates the popup at click time because those form IDs come from theme settings, and no
sniffer has registered them. The popup has the default priority of 0, so it does not open while a popup of priority 0 or
higher is open.
snippets/popup-logic.liquid also contains an inline script for collection templates that bypasses the monitor. On
DOMContentLoaded it calls Klaviyo's openForm with the form ID from the collection metafield
custom.klaviyo_form_id_desktop or custom.klaviyo_form_id_mobile, choosing at 768 pixels of window.innerWidth. When
routes.root_url has a language or market prefix, the snippet appends it to the metafield key, for example _zh_TW.
The script reads a cookie named for the device type but always writes view_klavyio_popup_mobile-<language>, with a
5-day expiry. On desktop the cookie it reads is never set, so the form opens on every collection page load.
Failure modes
No popup appears at all. The manager is turned off in theme settings, or the shopper already has a Klaviyo profile cookie and every registered definition skips itself.
A blacklisted popup still appears. Either the selector does not match, or the popup registered with the monitor and the monitor opened it. For a Klaviyo form, check the form ID, not a CSS class, because the snippet builds the selector from the ID. If the ID is right, turn the form off in Klaviyo.
Only one popup ever shows. This is often expected. The next popup waits 15 seconds after the first closes, and the limit is two per page view.
A popup flashes then disappears. It rendered before the monitor registered it, and the monitor then hid it. A sniffer usually found it late.
The offers link does nothing. A desktop or mobile form ID is missing from theme settings, or another popup was open when the shopper clicked.
A Klaviyo form opens on every collection page on desktop. This is the collection page script's cookie mismatch described above.
Source map
| Concern | File |
|---|---|
| Blacklist styles, config and the collection popup | snippets/popup-logic.liquid |
| Registration, first attempt and timer | src/entrypoints/popup-logic.ts |
| Monitor and its rules | src/components/popup-logic/index.ts |
| Timing | src/components/popup-logic/timer.ts |
| Interfaces | src/components/popup-logic/types.ts |
| Klaviyo detection and priority | src/components/popup-logic/sniffers/klaviyo.ts |
| Klaviyo show and hide, including the dismissed check | src/components/popup-logic/adapters/klaviyo.ts |
| Offers link | src/entrypoints/klaviyo-popup-trigger.ts |
| Cookie helpers used by definitions | src/components/utils/cookies.ts |