KirokuForms

Popup Forms & Triggers

Show a form in a modal at the moment a visitor is most likely to fill it in: as they move to leave, after a delay, once they scroll past a point, on a button click, or when a section comes into view.

Trigger types

A trigger decides when the modal opens. The engine supports eight types, the same eight the Display settings offer. Each takes an argument except exit, bare intent, and all:

  • exit: fires on the signals that someone is on their way off the page. On a computer that is the cursor leaving through the top of the window; on a phone it is switching away from the tab or flicking back to the top. All of them are listed under what counts as leaving.

    Start with exit on its own, because its signals are all on by default and between them they cover a visitor with a cursor and a visitor without one.

  • delay:8000: fires after the given number of milliseconds on the page (here, 8 seconds).

    Start with 8 seconds on a short landing page and 20 to 30 seconds on a long article, because the timer needs to outlast the time it takes to read enough of the page to know what you are offering.

  • inactivity:30000: fires after the given number of milliseconds with no interaction (here, 30 seconds). A mouse move, a click, a tap, a key press, a scroll or a touch restarts the countdown, and a click counts even when no pointer produced it, so a button pressed from the keyboard or through assistive technology keeps the visitor counted as present. A scroll counts whether the page itself moves or only a panel inside it that scrolls on its own. Unlike delay, which counts time on the page, this counts idle time since the last interaction.

    Start with 30 seconds, because a shorter idle window fires while the visitor is still reading the part of the page they stopped on, and a reader who has stopped to read is the one person you do not want to interrupt.

  • scroll:50: fires once the visitor has scrolled past the given percent of the page height (here, halfway down).

    Start with 50, because half the page is past the opening pitch on most layouts and still leaves room to act. Raise it on a long article so the form arrives after the argument; lower it on a short page where 50 percent is one screen.

  • click:#selector: fires when the visitor clicks any element matching the CSS selector. With an <a href> target the modal opens and the browser stays on the page; without JavaScript the link still navigates.

    Start with the id of the one button that already asks for the same thing, for example click:#book-a-demo, because a broad selector such as click:a opens the modal on every link on the page.

  • visible:#selector: fires when the element matching the selector scrolls into view.

    Start with the section that makes the case for the form, for example visible:#pricing, because by the time that section is on screen the visitor has read the argument for filling it in.

  • intent:hot (or bare intent / intent:warm): fires when VeritaMetrics' cookieless engagement detection reads the session as warm or hot. The site must run VeritaMetrics with warm-lead capture on (data-warm-lead="true"), which dispatches a veritametrics:intent browser event; this trigger listens for it and opens the form. No personal data is in the signal.

    Start with intent:warm, because hot is a narrow tier and a form that waits for it can go a whole session without opening. Move to intent:hot only once warm turns out to be too broad for what you are asking.

  • all: a composite AND. Join conditions with & (for example intent:warm&exit); the form opens only once every listed trigger has fired. Use it to greet engaged visitors who are leaving, warm intent and exit, rather than every passer-by.

    Start with two conditions that measure different things, for example intent:warm&exit, because a pair that measures the same thing twice (a delay and an idle window) cuts the audience without telling you anything new. In the form JSON the pair lives in and; the Display settings show which two conditions are in force.

Every trigger fires at most once per page load, click included: when the modal opens the engine drops its listeners and timers. For a button that reopens the form on every click, use data-kiroku-open below, which is not capped that way.

What counts as leaving

exit watches five departure signals. Only the first needs a cursor, which is why the trigger works on a phone. Any one of them opens the form, and the arming guard below applies to all five.

  • The cursor leaves through the top of the window. The desktop gesture: the tab strip, the address bar and the other tabs are all up there. A departure through the side or the bottom is ignored. The engine waits a second before believing it, so a cursor that comes straight back cancels it. Both the width of the top band and the length of that wait are settings (exitSensitivityPx and mouseLeaveDelayMs, above); they apply to this signal only.
  • The visitor switches away from the tab. Backgrounding the tab, switching to another tab, or locking the phone. No cursor involved, which is what makes this the main signal on a phone.
  • The window loses focus. Switching to another application, or clicking into the address bar. Covers every platform.
  • A fast scroll back to the top. The reach for the browser chrome a thumb makes. It has to be quick and it has to end near the top of the page: the same distance covered slowly is someone reading their way back up, and a single jump is a "back to top" button or a restored scroll position.
  • The address bar shortcut, Cmd+L. Whatever gets typed there next is somewhere other than your page. This one is macOS only: Ctrl+L is the same shortcut on Windows and Linux and is deliberately not watched, because pages sometimes bind Ctrl chords for themselves and we would rather miss a signal than steal a keystroke. Losing it costs little, since losing focus to the address bar already fires on every platform through the signal above.

How soon a popup can open

Each trigger holds the modal until its own condition is met. On top of that, the four triggers the engine infers from behaviour (exit, scroll, visible and intent) wait for a minimum time on the page before anything they see counts, 15 seconds by default. It is a guard rather than a trigger: when it elapses nothing opens, the trigger's own signal still has to arrive.

What the guard does to a signal that arrives inside it depends on whether the signal is a state or a gesture. A state is still true when the guard lifts and is read then, so a visitor already past the scroll mark opens the form as soon as it lifts. A gesture is over by then and is dropped: a cursor leaving the page or an intent event in the first 15 seconds does not open the form later, because replaying it would open the form at a moment the visitor did nothing. The same gesture made after the guard opens the form as usual.

The other three triggers are not held, because each one names its own moment. delay and inactivity open at a time you typed into the dashboard, so a floor would overrule it; click opens because the visitor pressed the element, and guarding that would leave your button dead for the first 15 seconds. all arms each half through the same path, so a half is held or not on its own.

To hold a path back for longer than the guard, combine the trigger with a delay: exit&delay:30000 opens on the exit that follows 30 seconds on the page. The two halves latch independently and in either order, so a departure gesture at 20 seconds waits for the timer to complete at 30. A gesture inside the first 15 seconds is the case above: the exit half is still under the guard, so it is dropped and the visitor has to make the gesture again.

Combine several triggers

Pass a comma-separated list to open the modal on whichever condition happens first. This example opens the form at the halfway scroll mark or as soon as the visitor clicks #demo-cta, whichever comes first:

Multiple triggers, first to fire wins
<script src="https://www.kirokuforms.com/embed.js"
  data-kiroku-form-id="YOUR_FORM_ID"
  data-kiroku-trigger="scroll:50, click:#demo-cta"
  async></script>

Open a popup on demand

Besides the saved trigger, you can open the modal yourself from a button, from your own JavaScript, or from a custom event. Pick whichever couples least with your page code.

From a button: data-kiroku-open

Put data-kiroku-open="YOUR_FORM_ID" on any element. A click opens that form in the modal. The handler is delegated from the document, so elements added after the script loads still work.

Button opens the popup
<script src="https://www.kirokuforms.com/embed.js" async></script>

<button data-kiroku-open="YOUR_FORM_ID">Contact sales</button>

From your code: window.KirokuForms.open()

Call window.KirokuForms.open(formId) when your own logic decides the moment is right. An unknown form id fails quietly and leaves the page untouched.

Programmatic open
window.KirokuForms.open("YOUR_FORM_ID");

From an event: kiroku:open

Dispatch a document-level kiroku:open CustomEvent with the form id in its detail. Your trigger code then stays decoupled from the embed:

Open via CustomEvent
document.dispatchEvent(
  new CustomEvent("kiroku:open", { detail: { formId: "YOUR_FORM_ID" } })
);

Override the trigger per placement

The form's saved trigger applies wherever you embed it. To use a different trigger for one placement, set data-kiroku-trigger on that script tag. It accepts the same values as the saved trigger: exit, delay:8000, inactivity:30000, scroll:50, click:#selector, visible:#selector, intent:warm, an & pair such as intent:warm&exit, and comma-separated lists.

Per-placement trigger override
<script src="https://www.kirokuforms.com/embed.js"
  data-kiroku-form-id="YOUR_FORM_ID"
  data-kiroku-trigger="click:#pricing-contact"
  async></script>

Keep a no-JavaScript fallback

When a click trigger targets a link, keep the link's href pointing at a real page (for example your contact page). Visitors with JavaScript get the modal; visitors without it follow the link, so both paths reach the form.

What the engine does not do

Two behaviours are fixed, so you can integrate against them without checking whether a form's settings changed them.

A decline is durable

A passive close (the X, Escape, a click on the backdrop) starts the frequencyDays window. An explicit decline is a stronger signal, so it starts its own longer one: the form stays shut for that visitor for 90 days, and that holds even when frequencyDays is 0 for per-session re-showing. Set settings.popup.decline.suppressDays to pick a different window, or 0 to switch the longer window off and fall back to frequencyDays.

The back button is left alone

We do not intercept the browser back button. The engine pushes no history entry and listens for no popstate, so Back goes to the previous page whether or not a popup is open or has been shown.

A history trap is the usual substitute for exit intent on a touch device, where there is no pointer to leave the top of the viewport. exit reads the tab switch, the window losing focus and the fast scroll to the top instead, so it has real signals to work from there and never has to hold anyone on the page to get one.

Next steps