Реалізація Exit Intent Popup для утримання користувача

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація Exit Intent Popup для утримання користувача
Середня
від 1 робочого дня до 3 робочих днів
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Реалізація Exit Intent Popup для утримання користувачів

Exit Intent — це паттерн, при якому спливаюче вікно з'являється в момент, коли користувач збирається покинути сторінку. На настільних комп'ютерах це виявляється за рухом мишки до верхньої границі вікна переглядача (до кнопки закриття вкладки або адресного рядка). На мобільних пристроях — за натисненням кнопки «назад» або за подією visibilitychange.

Виявлення на настільних комп'ютерах

Ключовий момент: реагувати не на будь-який рух вгору, а на швидкий рух до верху екрану з малою Y-координатою.

// exit-intent.ts
interface ExitIntentOptions {
  threshold?: number;       // px від верху, за замовчуванням 20
  delay?: number;           // мінімум секунд на сторінці перед показом
  cooldown?: number;        // ms до наступного показу (0 = показати один раз)
  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';

  // Перевірте, чи не показували вже в цій сесії / за період cooldown
  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) {
    // Рухаємось до верхнього краю
    if (e.clientY > threshold) return;
    // Достатньо швидко (швидкість руху — похідна позиції)
    if (!shouldShow()) return;

    triggered = true;
    lastTriggeredAt = Date.now();
    if (cooldown > 0) {
      sessionStorage.setItem(STORAGE_KEY, String(lastTriggeredAt));
    }

    onExit();
  }

  // Мобільна заміна: 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);
    },
  };
}

Компонент спливаючого вікна (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;             // наприклад, "Знижка 10% за кодом 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, // показувати один раз на день
      onExit: () => setVisible(true),
    });

    return () => detectorRef.current?.destroy();
  }, [delaySeconds]);

  useEffect(() => {
    const dialog = dialogRef.current;
    if (!dialog) return;

    if (visible) {
      dialog.showModal();
      // фокус на першому інтерактивному елементі
      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">
        {/* Кнопка закриття */}
        <button
          onClick={handleClose}
          aria-label="Закрити"
          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.com"
            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"
        >
          Ні, я йду
        </button>
      </div>
    </dialog>
  );
}

Аналітика показів і конверсій

Без вимірювань оптимізація спливаючого вікна неможлива. Мінімальний набір подій:

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,
    });
  }

  // власна аналітика
  fetch('/api/analytics/events', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    keepalive: true, // важливо для подій при закритті вкладки
    body: JSON.stringify({
      event: `exit_intent.${event}`,
      url: window.location.href,
      timestamp: new Date().toISOString(),
      ...meta,
    }),
  });
}

keepalive: true критично важливий для fetch у подіях, які можуть спрацьовувати при закритті вкладки — без цього браузер може скасувати запит.

Типові помилки при реалізації

Кілька типових проблем:

Спливаюче вікно при кожному відвідуванні — дратує, конверсія падає. Потрібен cooldown мінімум на день через localStorage/cookie.

Показ на мобільних через mouseleave — подія не спрацьовує. Потрібен окремий детектор через visibilitychange або pagehide.

Відсутність полізаповнення <dialog>HTMLDialogElement підтримується у всіх сучасних браузерах (Chrome 37+, Firefox 98+, Safari 15.4+), але для підтримки старіших — використовуйте dialog-polyfill або користувацьку реалізацію через aria-modal.

Строки виконання

Базове спливаюче вікно з детектором і аналітикою — один-два дні. A/B тестування кількох варіантів з автоматичним вибором переможця — ще два-три дні.