Updated · 8 min read
Email popup timing: fire on a signal, not on a stopwatch
We shipped a lead-capture modal on this site with a 2.5-second delay after first paint, on the reasoning that the page should land before we ask for anything. It read as considerate. What it actually did was wait for visitors to open the chat widget, type a question, and then drop an aria-modal dialog with a full-screen backdrop on top of the conversation they had just started. The E2E suite caught it before we did. This guide is the version of popup timing we should have written first: which signals to defer on, why exit intent is weaker than its reputation, how to budget impressions, and what you owe a visitor once you have declared the rest of the page inert.

By Justin Williames
Founder, Orbit · 10+ years in lifecycle marketing
The 2.5-second timer, and what it was actually doing
Our modal had sensible-looking rules. Once per browser session. Never again after a submit. Suppressed on admin, login and API paths. Consent defaults resolved by jurisdiction, in parallel, so a slow lookup never stranded anyone. And a 2.5-second delay after first paint, with a comment explaining that the page should land before we ask.
Every rule in that list is about whether to ask and who to ask. None of them asked whether the visitor was busy. The site also runs a chat widget, and the two components knew nothing about each other. A visitor who opened the chat and typed a question was, at the 2.5-second mark, still on the same page load — so the timer fired, the dialog opened over the chat panel, and because it carried aria-modal and a full-screen backdrop, it swallowed every click aimed at the answer underneath.
The failure surfaced as an E2E test complaining that a result card could not be clicked because a role="dialog" element was intercepting pointer events. That reads as a chat bug. Somebody debugging it would open the chat component and find nothing wrong, because nothing was wrong there.
A visitor who opens your chat and types a question is the most engaged person on the site. A blind timer is calibrated to interrupt exactly that person.
The fix was small and the thinking behind it is the whole guide. The delay stopped being a one-shot setTimeout and became a condition we re-check: while the chat panel is in the DOM, wait; show on the first tick after it closes. The modal now asks at a moment when the visitor has finished what they came for, which was always the better moment to ask.
Why a timer selects for the wrong visitor
A timer measures one thing: seconds since paint. It knows nothing about the person. Two visitors sitting at the 2.5-second mark can be in completely different states — one has already decided the page is not for them, the other is three sentences into a question — and the timer treats them identically.
The selection effect makes it worse. The visitor who was going to bounce has often already gone by the time the timer fires, so they never see the modal at all. The population that does see it is skewed towards people who stayed, and people who stayed are disproportionately mid-task. A delay tuned longer to be politer makes this stronger, not weaker: every extra second filters harder for the visitor who is still doing something.
Then there is the budget. Our modal writes a dismissal timestamp and honours a 30-day TTL on the same device, which is the right discipline — a "no" should mean something. But it also means one mistimed impression spends the entire ask for a month. The real cost of interrupting is the dismissal plus every better moment in the next 30 days that you have now forfeited.
That reframes the design question away from "how long should the delay be?". No number answers that one, because elapsed time is not what you care about. The question worth asking is which moment in this session is the cheapest one to spend the single ask you get.
The signals worth deferring on
All of the useful signals answer the same question: is this visitor mid-task right now? They are cheap to read from the DOM, and each one has an obvious deferral rule.
| Signal | What it means | What to do |
|---|---|---|
| Chat or support panel open | Mid-conversation, expecting a reply | Wait for it to close, then ask on the next tick |
| Focus inside any input or textarea | Typing something they intend to keep | Wait for blur, plus a short idle gap |
| Live text selection on the page | Copying, quoting, or comparing | Hold until the selection clears |
| Video or audio playing | Watching, not browsing | Stay shut until paused or ended |
| Scrolled into a how-to or code block | Reading for a specific answer | Push to the end of the section, or to exit |
| On a signup, checkout or auth path | Converting on something better | Suppress entirely — don't queue it |
| document.visibilityState is hidden | Tab is in the background | Never open into a hidden tab; re-check on focus |
The implementation shape is identical in every row: replace the one-shot timer with a condition you keep checking. Ours polls once a second while the chat is open, which is unglamorous and completely adequate; a MutationObserver or an event the other component already emits is tidier where you control both sides. The runtime cost of either is irrelevant next to the cost of interrupting a conversation.
Two rules keep this from sprawling. First, deferral is not queueing — a suppressed surface like checkout should drop the ask for the session, not fire it the moment the visitor lands on the confirmation page. Second, every deferral needs a ceiling, or a visitor who leaves a form field focused and walks away never sees the modal and never gets counted; time-box the wait and let the session end without the ask.
Be honest about the positive signals too. Scroll depth and time-on-page do correlate with interest, and that is exactly why they are dangerous — a visitor eighty percent down a long guide is interested and busy. The condition worth building is not "engaged enough to ask". It is "has seen enough to know what we are, and is not currently doing something". The Growth Marketing skill covers where capture surfaces sit in the wider channel mix; this guide is only about the moment.
Exit intent, assessed honestly
Exit intent watches the pointer leave the top edge of the viewport and infers that the visitor is heading for the tab bar or the close button. As a trigger it beats a stopwatch comfortably, and it is weaker than the way it gets sold.
Three limits, all mechanical. It is a mouse heuristic, so on touch devices the signal does not exist — and the common substitutes, back-button hijacking and scroll-up detection, are both worse than doing nothing, because one of them breaks a browser control the visitor owns. Misfires are constant: the pointer also travels to the top edge for a bookmark, the URL bar, a second monitor, or another tab the visitor fully intends to come back from. And by construction it fires at the moment someone has decided to leave, which is the cheapest moment to interrupt and also the moment they are least inclined to type an address.
Where we land: exit intent is a reasonable secondary trigger on pointer devices, layered under the mid-task deferrals above, and never worth hijacking the back button for. The trade-off is explicit — you accept fewer impressions in exchange for a much lower rate of interrupting someone mid-task, and the impressions you lose are mostly the ones that were going to be dismissed anyway.
Frequency discipline: three flags, not one
A popup needs three separate pieces of state, and teams routinely ship one. Ours keeps all three: a session flag so it appears at most once per browser session, a permanent submitted flag so anyone who gave you their address is never asked again, and a dismissal timestamp with a 30-day TTL so a "no" is honoured for a meaningful stretch.
The dismissal TTL is the one that gets forgotten, and it is the one that decides whether your site feels like it is listening. A session-only flag means the same person is asked again tomorrow, and the day after, which converts a mild irritation into a policy. The exact window matters less than having one — pick something long enough that a returning visitor notices you remembered.
Two implementation details worth copying. Every storage read is wrapped so that a visitor with storage blocked still gets a working site; suppression is best-effort by design, and failing towards "show the site" rather than "throw" is the right default. And the path exclusions are absolute, not deferred: admin, login, API routes and the editor app never render the modal at all, because a lead-capture prompt on an internal surface is pure noise.
Consent belongs in the same block of thinking. Ours resolves jurisdiction while the delay runs, defaults the marketing checkbox off where opt-in is required and on where it isn't, and fails closed to opt-in when geo resolution fails. That is the floor, not a feature. An address captured through an interruption tends to behave like one downstream, which is a list-quality problem before it is a deliverability problem — the list hygiene policy guide covers what to do with the addresses once they arrive, and the welcome sequence guide covers earning the second impression.
What aria-modal actually commits you to
Setting role="dialog" with aria-modal="true" tells assistive technology that everything outside the dialog is inert. The browser does not deliver on that for you. You do, and there are five duties.
Move initial focus into the panel when it opens, usually to the first field. Trap Tab inside it, wrapping from last to first and — the half people skip — from first back to last on Shift+Tab. Close on Escape, and count that as a dismissal so the TTL is written. Restore focus to whatever element had it before the dialog opened, which is WCAG 2.4.3 and the difference between a keyboard user carrying on and a keyboard user starting the page again. Lock body scroll while it is open, and point aria-labelledby at the title so the dialog announces as something rather than as a dialog.
The chat collision made the last point concrete in a way a checklist never does. aria-modal is a claim about the entire page, and the backdrop enforces it with pointer events. When our modal opened over a live chat, the markup was not lying — it really had rendered that conversation inert, for everyone, exactly as advertised. Which means the fix was never a z-index or a smaller backdrop. The fix was not opening.
Then lock it down with a test that names the behaviour. That test opens the chat, types a question, waits well past the old 2.5-second mark, and asserts the dialog has a count of zero, with a failure message that says the modal opened over an active conversation and points at the polling logic. Our chat-scenario suite already failed when this regressed, but with "cannot click a result card" — a true statement that sends the next person to the wrong file. A test that explains itself is worth more than a test that merely catches. The same logic applies to reading experiment results honestly: see A/B testing discipline and false-positive prevention before you declare a timing change a win.
Ship three things this week, in order. Gate the existing delay on "no open chat, no focused input". Add a dismissal TTL if you only have a session flag. Then audit the dialog against the five duties above. None of it is a redesign, and together they stop your capture surface from spending its one ask on the visitor who was already convinced. For the wider question of asking people for data without making it feel like surveillance, personalisation without the creep factor is the companion piece.
Read to the end
Scroll to the bottom of the guide — we'll tick it on your reading path automatically.
Frequently asked questions
- How many seconds should an email popup wait before showing?
- Elapsed time is not the thing you care about, so no number answers this well. Fire on a condition instead: the visitor has seen enough of the page to know what you are, and is not currently mid-task — no open chat, no focused input, no live text selection, no playing media. If you must keep a delay, treat it as a floor that a condition still has to clear.
- Is exit intent better than a timed popup?
- As a secondary trigger on pointer devices, yes. It has three real limits: it doesn't exist on touch, it misfires whenever the pointer heads for a bookmark or another tab, and it fires at the moment someone has already decided to leave. Use it layered under mid-task deferrals, and don't reach for back-button hijacking as the mobile substitute — breaking a browser control the visitor owns costs more than the address is worth.
- What is a reasonable dismissal window?
- Long enough that a returning visitor notices you remembered. We settled on 30 days per device, stored as a timestamp rather than a boolean so the window is enforceable. The specific length matters less than having a TTL at all — a session-only flag means you ask the same person again tomorrow, which turns one irritation into a standing policy.
- Should the popup appear on every page?
- Exclude admin, login and API surfaces outright, and suppress it on any conversion path — signup, checkout, an in-progress form — where the visitor is already doing the more valuable thing. Suppression on those paths should drop the ask for the session rather than queue it to fire the moment they land somewhere else.
- Do popups have to be modal dialogs at all?
- No, and the alternative is worth weighing. An inline block or a corner slide-in can't swallow clicks and carries none of the aria-modal obligations. The trade-off is real: a modal is harder to ignore, which is the point and also the risk. If you do choose a modal, you owe the visitor initial focus, a Tab trap, Escape to close, focus restored on close, and a scroll lock.
- How do I know if changing the timing helped?
- Measure the downstream, not the impression. Impressions go down when you defer, by design, so an impression-count comparison will always look like a loss. Compare submission rate per session and the behaviour of the addresses afterwards — confirmed opens, early complaints, early unsubscribes — and read the result with normal experiment discipline rather than calling it after a good week.
This guide is backed by an Orbit skill
Related guides
Browse allTransactional emails: the highest-engagement messages you ignore
Order confirmations, password resets, receipts, shipping updates. Transactional emails post open rates two to three times higher than marketing sends — and most lifecycle teams have never touched them. Effort is going to the wrong place.
Birthday and anniversary emails: the easy wins most programs don't run
Birthday and anniversary emails are low-effort, high-response lifecycle triggers that most programs never set up. Here's the playbook — what to send, what to offer, and how to handle the 40–70% of users who didn't give you a birthday.
Personalisation that doesn't feel creepy
There's a line between personalisation that earns trust and personalisation that breaks it. It's not where most people think it is — it's about how you signal what you know, not what you know. Here's the line, how programs cross it without noticing, and the patterns that keep you on the right side.
Onboarding flows: signup to activated
Most onboarding programs fail in the same three ways — no activation metric, no awareness of what the user just did in-product, and a sequence that won't stop once the user has clearly activated. Fix those three and the program starts moving signups to activated users in numbers you can actually defend.
Win-back flows: 12 patterns that earn their place
Win-back is the highest-ROI program most lifecycle teams underbuild. Twelve patterns that work, when each one fits, and the sunset policy that stops the program quietly eating your sender reputation.
List hygiene: the six-rule policy
List hygiene isn't cleanup; it's a continuous policy that runs automatically. Here's the six-rule policy every lifecycle program should have written down, each tied to a specific deliverability outcome.
Found this useful? Share it with your team.
You finished the playbook. Get the next.
New guides and product updates land in your inbox when they ship. One list, real lifecycle work, unsubscribe the second it stops being useful.
Guides and Orbit updates only. No sequences, no selling your address.
Use this in Claude
Claude can run this playbook for you.
Orbit is a free extension for Claude Desktop — no licence key, no card — that runs the lifecycle work you just read about. You've read how it works; Orbit hands Claude the same playbook as a skill it can execute: discovery, build, QA, push, on your own ESP.
Download Orbit — free