The guide
A leaving visitor owes you nothing, so ask once, close easily and never fake a deadline
Exit intent is a guess drawn from one mouse movement, and touch screens offer no such signal at all. A message built on that guess has to earn its interruption by being short, honest and easy to dismiss.
- 1,818 dark pattern instances found on about 11K shopping sites
- 4.5:1 WCAG 2.2 minimum contrast for ordinary text
- 24 by 24 CSS px smallest pointer target WCAG 2.2 accepts at level AA
- 17:00 UTC 18:00 on 30 November 2026 in Berlin, the worked example
Read the full guide Fold the guide
Nobody has ever left a web page by announcing it. There is no event called “leaving”, no flag a browser raises when a reader has decided they are done. What a page can see is much smaller: a pointer that crosses the edge of the window, a tab that goes into the background, a scroll that stops. Everything sold as “exit intent” is an inference from one of those small facts, and the honest way to build on an inference is to treat it as a guess.
That framing changes what a message at the door should look like. If you are guessing, you ask once. You make the answer free. You do not make the reader work to get rid of you, and you do not invent a reason for them to hurry.
What the browser can actually see
On a computer with a mouse or a trackpad, the useful fact is the pointer leaving the document. MDN describes the mouseleave event as firing when the cursor of a pointing device moves out of an element, and notes that it does not bubble, while the related mouseout event does. A script that listens on the document and sees the pointer go past the top edge, with nothing underneath it to move into, has observed a hand heading for the tab bar, the address bar or the close button. It has not observed a decision.
The hand might be going for another tab to compare prices, or for a bookmark, or nowhere at all. The signal is real; the meaning is assumed. That is why a sensible version also waits for a minimum time on the page, so that a reader who arrives and immediately reaches for the address bar to fix a typo is not greeted by a box. It is also why the message should never appear on first paint, before the reader has seen anything they might want to stay for.
On a phone or a tablet the signal does not exist. Media Queries Level 5 defines the hover feature as “none” when the primary pointing device cannot hover, and names touchscreens as the example; MDN’s page on the pointer feature calls a finger on a touchscreen a coarse pointer of limited accuracy. A finger does not drift toward the top of the screen before a reader switches apps. It simply leaves. Scripts that claim to detect exit intent on touch devices are usually watching for something else entirely: a quick scroll upward, the back gesture, the page becoming hidden.
Each of those is worse than useless as a trigger. MDN describes the visibilitychange event firing with a hidden state when a user switches tabs, minimises the browser or moves to another app, and calls that transition a good point to stop work, not to start it. A message opened at that moment is shown to nobody. Trapping the back button to show a message first is a fight with the browser’s own navigation, and the reader always notices.
The beforeunload prompt is even less suited to the job: MDN warns that it is not reliably fired, especially on mobile, that some browsers keep a page with such a listener out of the back and forward cache, and that it should be used only to warn about unsaved changes.
So the honest offer on a touch screen is a different rule altogether, stated as what it is. After a reader has spent a fair amount of time on the page and scrolled a fair way through it, they have shown enough interest that one question is not an ambush. It is not exit intent, and the page that offers it should not pretend otherwise.
Why an interruption costs more than it looks
A modal message takes over the screen. That is its whole point, and it is also its cost. The reader was doing something, and now they must stop, read, decide and dismiss before they can carry on. Every second of that is spent from a store of goodwill that the page did nothing to fill.
The cost rises sharply when the interruption is dishonest. The FTC staff report Bringing Dark Patterns to Light, published in September 2022, sorts manipulative designs into families, and several of them describe the door message exactly. Under urgency it lists the “Baseless Countdown Timer”, a fake clock that goes away or resets when it runs out, and the “False Limited Time Message”, an offer said to end soon with no deadline or one that resets. Under asymmetric choice it lists “Confirm Shaming”, using shame to steer people away from a choice. Under coerced action it describes “Nagging”, repeated requests that will not take no for an answer.
These are not rare. Researchers who crawled about 53,000 product pages on roughly 11,000 shopping websites in 2019 reported 1,818 instances of dark patterns across 15 types and 7 broader categories, found 183 websites engaging in deceptive practices, and identified 22 third parties offering such patterns as ready-made services. The Wikipedia article on dark patterns gives the plainest description of confirmshaming: wording the option to decline a newsletter so that refusing feels shameful.
The common thread is that each pattern spends trust the reader did not agree to lend. A countdown that restarts for every visitor is a lie that the visitor can catch with a second browser window. A close button labelled with a small insult is a lie about what kind of person says no. Neither one survives a reader who looks twice, and the readers most likely to look twice are often the ones a business most wants to keep.
What the native dialog gives you for free
The good news is that the honest version is also the simpler one to build, because browsers now do most of the work. The HTML Standard’s dialog element, opened with its showModal method, makes the rest of the document blocked by the modal dialog, which turns the page behind it inert. It places the dialog in the top layer above everything else, remembers which element had the focus, and moves the focus inside. A close request, which on a keyboard means Escape, closes it, and closing returns the focus to where it was.
Before this element was widely available, every one of those behaviours was written by hand, and most hand-written versions got at least one of them wrong. Focus escaped to the page behind. Escape did nothing. Screen readers kept reading the article underneath. The WAI-ARIA Authoring Practices pattern for modal dialogs still spells out the expected keyboard behaviour: Tab and Shift with Tab wrap inside the dialog, Escape closes it, and the dialog’s tab sequence should include a visible button that closes it. It also asks for the dialog to be named by its visible title.
The HTML Standard explains how initial focus is chosen. If an element in the dialog carries autofocus it gets the focus; otherwise the first focusable element does. Putting a plain, visible close button first in the dialog therefore does two jobs at once. Keyboard users land on the way out, and nobody has to search for it. Moving the focus straight into an email field is a choice the owner can make deliberately, but it should not be the default, because it assumes the answer before the question has been read.
WCAG 2.2 supplies the remaining numbers. Ordinary text needs a contrast ratio of at least 4.5:1 against its background, computed from the relative luminance of the two colours; the ratio runs from 1:1 to 21:1. A pointer target must be at least 24 by 24 CSS pixels, though larger is kinder. Focus must not be entirely hidden by author content. Nothing should flash, and anything that moves should be stoppable, which for a door message simply means it should not move. A fade of a fifth of a second is pleasant, but it belongs only where the reader has not asked their system for reduced motion.
Here is a worked example you can reproduce. Take dark text #777777 on a white background: the WCAG formula gives 4.47:1, just under the line, so the builder refuses it. Darken the text by one step to #767676 and the ratio becomes 4.54:1, which passes. The builder’s own example uses #2b1d24 on #fffaf5, which comes out at 15.52:1. Set a deadline of 18:00 on 30 November 2026 in the Europe/Berlin zone and the tool prints it as that date and stores the instant as 17:00 UTC, because Berlin is one hour ahead of UTC in winter.
Asking once, and real deadlines
“Once” needs a memory. The simplest memory a page has is local storage, which MDN describes as saved across browser sessions for the page’s origin, with no expiration time. The smallest thing worth storing is the time the message was last shown. No identifier, no history, just one number that lets the script say “not again until the period is over”.
Storage can fail. MDN notes that access can throw a security error, for instance for a document on a file or data address, and private windows clear it when they close. When storage is unavailable the right fallback is the strictest one that still works: show the message at most once per page view, and never again after it is closed. A script that falls back to showing the message on every trigger because it cannot remember is punishing the reader for their browser settings.
Even one timestamp is still storage on the reader’s device, and rules about that vary by place. The UK Information Commissioner’s Office, for example, lists web storage among the storage and access technologies that its electronic communications rules cover, and explains that they may be used in particular circumstances or with valid consent. Whether a frequency cap falls inside an exception is a question for the site owner and their own adviser, not for a code generator.
Then there is the deadline. A real offer can end, and saying so is useful information, as long as the date is a date. The honest form prints the moment the offer ends in a time zone the reader can check, and once that moment has passed the message simply stops appearing. What it never does is count down from a number chosen per visitor, because that is precisely the baseless timer the FTC report describes.
Writing the result as plain markup, one style block and one short script also keeps it compatible with a strict Content Security Policy. Level 3 of that specification defines a nonce source, a fresh random value the server places both in the policy header and on each inline script or style, and it requires the server to generate a new value for every response. A message built this way needs no third-party script, makes no request of its own and posts a sign-up form, if it has one, straight to an address the owner controls.
None of this guarantees that anyone will sign up. A door message cannot know why a reader came or why they are leaving, and on a phone it cannot know they are leaving at all. What it can promise is narrower and more durable: it will ask once, it will tell the truth about time, and it will step aside the moment it is told to.
Sources
Every source consulted for this guide. Links checked October 20, 2026.
- HTML Standard, 4.11 Interactive elements WHATWG
- Dialog (Modal) Pattern W3C WAI-ARIA Authoring Practices Guide
- Web Content Accessibility Guidelines (WCAG) 2.2 W3C
- Bringing Dark Patterns to Light, Staff Report, September 2022 US Federal Trade Commission (Internet Archive copy)
- Dark Patterns at Scale: Findings from a Crawl of 11K Shopping Websites Mathur, Acar, Friedman, Lucherini, Mayer, Chetty and Narayanan, arXiv
- Dark pattern Wikipedia
- Element: mouseleave event MDN Web Docs
- pointer CSS media feature MDN Web Docs
- Media Queries Level 5 W3C
- Window: beforeunload event MDN Web Docs
- Document: visibilitychange event MDN Web Docs
- Window: localStorage property MDN Web Docs
- What are storage and access technologies? UK Information Commissioner's Office
- Content Security Policy Level 3 W3C