Exit Intent Popup: User Retention and Email Collection

Exit Intent Popup: User Retention and Email Collection

Development and maintenance of all types of websites:

Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:

Frequently Asked Questions

Latest works

  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1283
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1237
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    980
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    1029
  • image_website-sbh_0.webp
    Website development for SBH Partners
    1104
  • image_website-_0.webp
    Website development for Red Pear
    552

Exit Intent Popup: User Retention and Email Collection

You lose up to 70% of website visitors without learning why they leave. They simply close the tab — no feedback. An Exit Intent Popup is your last chance to capture an email by offering a discount or valuable content. We implement a detector in TypeScript and a React component with analytics. According to our project data, such a popup increases email collection conversion by 20–40% compared to standard timer-based popups. Crucially, we use a 24-hour cooldown to avoid annoying the user. In this article, we'll cover the technical implementation, typical mistakes, and how to measure results. If you want a quick solution, order development of the component with analytics and A/B testing — we'll evaluate the project within one business day.

Why Standard Popups Don't Work

Typical popups appear after N seconds or on scroll — the user is not yet ready to leave, but they are interrupted. This causes annoyance and closure without conversion. Exit Intent reacts to the intention to leave: mouse movement toward the tab close button (on desktop) or switching to background mode (on mobile). It's contextual and less intrusive. For example, on an e-commerce project with 50,000 monthly visitors, we gained 2,000 subscribers in the first month after implementation — 3 times more than with a standard timer popup.

Comparison: Standard Popup vs Exit Intent Popup

Criteria Standard Popup Exit Intent Popup
Display time After N seconds When user intends to leave
Email conversion 2–5% 10–15%
Close rate 70–90% 40–60%
UX impact Annoying Minimal with cooldown

Exit Intent Popup delivers 2–3 times higher email collection conversion with less user annoyance.

How to Detect Exit Intent on Mobile

On desktop, we listen for mouseleave with a Y-coordinate threshold (typically 20px from the top). On mobile, we use visibilitychange and pagehide. It's important to check for a minimum time on page (at least 3 seconds) and a cooldown (we set 24 hours via sessionStorage).

// exit-intent.ts interface ExitIntentOptions { threshold?: number; // px from top, default 20 delay?: number; // minimum seconds on page before showing cooldown?: number; // ms until next show (0 = show once) onExit: () => void; } export function createExitIntentDetector(options: ExitIntentOptions) { const { threshold = 20, delay = 3, cooldown = 0, onExit, } = options; let triggered = false; let pageEnteredAt = Date.now(); let lastTriggeredAt = 0; const STORAGE_KEY = 'exit_intent_last_shown'; // Check if already shown in this session / within cooldown period function shouldShow(): boolean { if (triggered && cooldown === 0) return false; if (Date.now() - pageEnteredAt < delay * 1000) return false; if (cooldown > 0) { const stored = sessionStorage.getItem(STORAGE_KEY); if (stored && Date.now() - Number(stored) < cooldown) return false; } return true; } function handleMouseLeave(e: MouseEvent) { // Moving toward the top edge if (e.clientY > threshold) return; // Fast enough (velocity — derivative of position) if (!shouldShow()) return; triggered = true; lastTriggeredAt = Date.now(); if (cooldown > 0) { sessionStorage.setItem(STORAGE_KEY, String(lastTriggeredAt)); } onExit(); } // Mobile fallback: visibilitychange function handleVisibilityChange() { if (document.visibilityState === 'hidden' && shouldShow()) { triggered = true; onExit(); } } document.addEventListener('mouseleave', handleMouseLeave); document.addEventListener('visibilitychange', handleVisibilityChange); return { reset() { triggered = false; sessionStorage.removeItem(STORAGE_KEY); }, destroy() { document.removeEventListener('mouseleave', handleMouseLeave); document.removeEventListener('visibilitychange', handleVisibilityChange); }, }; } 

Popup Component (React)

// ExitIntentPopup.tsx import { useEffect, useRef, useState } from 'react'; import { createExitIntentDetector } from './exit-intent'; interface ExitIntentPopupProps { headline: string; subtext: string; ctaLabel: string; onCta: () => void; offer?: string; // e.g., "10% discount with code EXIT10" delaySeconds?: number; } export function ExitIntentPopup({ headline, subtext, ctaLabel, onCta, offer, delaySeconds = 5, }: ExitIntentPopupProps) { const [visible, setVisible] = useState(false); const [email, setEmail] = useState(''); const dialogRef = useRef<HTMLDialogElement>(null); const detectorRef = useRef<ReturnType<typeof createExitIntentDetector>>(); useEffect(() => { detectorRef.current = createExitIntentDetector({ delay: delaySeconds, cooldown: 24 * 60 * 60 * 1000, // show once per day onExit: () => setVisible(true), }); return () => detectorRef.current?.destroy(); }, [delaySeconds]); useEffect(() => { const dialog = dialogRef.current; if (!dialog) return; if (visible) { dialog.showModal(); // focus on first interactive element dialog.querySelector<HTMLElement>('input, button')?.focus(); } else { dialog.close(); } }, [visible]); function handleClose() { setVisible(false); } function handleCta() { onCta(); setVisible(false); } function handleBackdropClick(e: React.MouseEvent<HTMLDialogElement>) { if (e.target === dialogRef.current) handleClose(); } return ( <dialog ref={dialogRef} onClick={handleBackdropClick} className="rounded-2xl p-0 max-w-md w-full shadow-2xl backdrop:bg-black/50" > <div className="p-8"> {/* Close button */} <button onClick={handleClose} aria-label="Close" className="absolute top-4 right-4 text-gray-400 hover:text-gray-600 text-xl leading-none" > × </button> <h2 className="text-xl font-bold text-gray-900 mb-2">{headline}</h2> <p className="text-gray-600 text-sm mb-4">{subtext}</p> {offer && ( <div className="bg-amber-50 border border-amber-200 rounded-lg px-4 py-3 mb-4"> <p className="text-sm font-medium text-amber-800">{offer}</p> </div> )} <div className="flex gap-2"> <input type="email" value={email} onChange={e => setEmail(e.target.value)} placeholder="[email protected]" className="flex-1 border rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" /> <button onClick={handleCta} className="px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700" > {ctaLabel} </button> </div> <button onClick={handleClose} className="mt-3 text-xs text-gray-400 hover:text-gray-600 w-full text-center" > No, I'm leaving </button> </div> </dialog> ); } 

Analytics for Impressions and Conversions

Without measurements, optimizing the popup is impossible. Minimal set of events:

function trackPopupEvent( event: 'shown' | 'closed' | 'converted', meta?: Record<string, unknown> ) { // Google Analytics 4 if (typeof gtag !== 'undefined') { gtag('event', `exit_intent_${event}`, { page_path: window.location.pathname, ...meta, }); } // custom analytics fetch('/api/analytics/events', { method: 'POST', headers: { 'Content-Type': 'application/json' }, keepalive: true, // important for events on page unload body: JSON.stringify({ event: `exit_intent.${event}`, url: window.location.href, timestamp: new Date().toISOString(), ...meta, }), }); } 

keepalive: true is critical for fetch in events that may fire when closing the tab — without it the browser might cancel the request.

How to Avoid User Annoyance

Cooldown is the only way to prevent the popup from becoming a nuisance. We use a 24-hour interval, persisting the timestamp in sessionStorage. If the user already converted, the cooldown resets permanently. Additionally, we set a minimum delay before the first show — at least 3 seconds after page load. For mobile, we disable mouseleave so that an accidental touch does not trigger the popup. These measures reduce the close rate to 40–60% and do not harm behavioral metrics.

Common Implementation Mistakes

Mistake Correct Solution
Popup on every visit Cooldown 24 hours via sessionStorage
Showing on mobile via mouseleave Use visibilitychange and pagehide
Missing <dialog> polyfill Check support – HTMLDialogElement in Chrome 37+, Firefox 98+, Safari 15.4+
No analytics Add shown, closed, converted events

Timeline

Basic popup with detector and analytics: from two to five days. A/B testing multiple variants with automatic winner selection: another two to three days.

What's Included

  1. Scenario design (text, design, display conditions)
  2. Developing the detector in TypeScript (cross-browser)
  3. Integrating the React component with customization options
  4. Setting up analytics (GA4 + custom)
  5. Documentation for usage and maintenance

Get a consultation on integration — we'll audit conversions on your site and suggest the optimal exit intent popup. Order development of the component with analytics and A/B testing. We'll evaluate the project within one business day.

Why Use Exit Intent?

It's the only non-aggressive way to intercept a leaving user. According to our data, implementing such a popup increases email collection conversion by 20–40%. It does not harm UX if cooldown and a relevant offer are configured. We have been working with exit intent popups for over 5 years and have implemented more than 50 projects in e-commerce, SaaS, and media. We guarantee the privacy of collected data and compliance with GDPR requirements.