Exit Intent Popup: User Retention and Email Collection

Visitors leave your site without leaving contacts, and you don't know why. We build exit intent popups that trigger when a user intends to leave, offering a discount or useful content in exchange for an email. Our team delivers the project turnkey—from exit detection to analytics and A/B testing—with ongoing support.

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

  • Development of a web application for FEEDME
    Development of a web application for FEEDME
    1342
  • Development of an online store for the company FURNORO
    Development of an online store for the company FURNORO
    1304
  • Development of a web application for Enviok
    Development of a web application for Enviok
    1047
  • CRM development for Chasseurs
    CRM development for Chasseurs
    1094
  • Website development for SBH Partners
    Website development for SBH Partners
    1169
  • Website development for Red Pear
    Website development for Red Pear
    593

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.