Реалізація триггерних pop-up вікон за поведінкою користувача на веб-сайті
Триггерний поп-ап — це всплувальне вікно, яке показується не за таймером, а за конкретною дією користувача: рух мишею до закриття вкладки, довга пауза на певному блоці, прокрутка до нижньої частини сторінки. Правильно налаштований поп-ап підвищує конверсію на 2-5%, неправильний — дратує і виганяє людей.
Типи тригерів
Основні тригери, які варто реалізовувати:
- Exit intent — мышка рушається до верхнього краю екрану, користувач збирається закрити вкладку
- Scroll depth — прокрутка досягла N% сторінки
- Time on page — користувач провів на сторінці X секунд
- Inactivity — нема руху миші і кліків понад Y секунд
- Element visibility — конкретний блок потрапив у viewport
- Click intent — наведення на елемент без кліку
Exit Intent
Найефективніший тригер для лендингів. Визначається за швидкістю і напрямком руху курсора:
class ExitIntentDetector {
private threshold = 10; // піксели від верхнього краю
private sensitivity = 50; // px/ms — мінімальна швидкість руху вгору
private triggered = false;
private lastY = 0;
private lastTime = 0;
constructor(private onExit: () => void) {
document.addEventListener('mousemove', this.handleMouseMove.bind(this));
}
private handleMouseMove(e: MouseEvent): void {
if (this.triggered) return;
const now = Date.now();
const deltaY = e.clientY - this.lastY;
const deltaTime = now - this.lastTime;
const velocityY = deltaY / deltaTime; // px/ms, негативне = рух вгору
if (e.clientY < this.threshold && velocityY < -this.sensitivity) {
this.triggered = true;
this.onExit();
}
this.lastY = e.clientY;
this.lastTime = now;
}
reset(): void {
this.triggered = false;
}
}
// Використання
const detector = new ExitIntentDetector(() => {
showPopup('exit_offer');
});
На мобільних пристроях exit intent не працює — немає события mousemove. Замість цього використовуйте тригер за натисканням кнопки «Назад» через popstate:
// Мобільний exit: пуш стану в історію, ловимо вихід
history.pushState({ popup: true }, '');
window.addEventListener('popstate', (e) => {
if (!e.state?.popup) {
showPopup('mobile_exit_offer');
history.pushState({ popup: true }, '');
}
});
Scroll Depth тригер
function onScrollDepth(percentage: number, callback: () => void): () => void {
let fired = false;
const handler = () => {
if (fired) return;
const scrolled = window.scrollY + window.innerHeight;
const total = document.documentElement.scrollHeight;
if (scrolled / total >= percentage / 100) {
fired = true;
callback();
}
};
window.addEventListener('scroll', handler, { passive: true });
return () => window.removeEventListener('scroll', handler);
}
// Поп-ап при прокруці 70% сторінки
onScrollDepth(70, () => showPopup('mid_page_offer'));
Element Visibility (Intersection Observer)
Показувати поп-ап коли користувач прокручує до блоку цін:
function onElementVisible(selector: string, callback: () => void): void {
const el = document.querySelector(selector);
if (!el) return;
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
observer.disconnect();
callback();
}
},
{ threshold: 0.5 } // елемент видимий на 50%
);
observer.observe(el);
}
onElementVisible('#pricing-section', () => showPopup('pricing_helper'));
Менеджер стану поп-апів
Критично важливо не показувати один поп-ап кілька разів і не показувати одночасно кілька:
class PopupManager {
private shown = new Set<string>(
JSON.parse(localStorage.getItem('shown_popups') ?? '[]')
);
private currentPopup: string | null = null;
canShow(id: string, cooldownDays = 7): boolean {
if (this.currentPopup) return false; // уже відкритий інший
const key = `popup_shown_${id}`;
const lastShown = localStorage.getItem(key);
if (!lastShown) return true;
const daysSince = (Date.now() - parseInt(lastShown)) / 86400000;
return daysSince >= cooldownDays;
}
show(id: string): void {
if (!this.canShow(id)) return;
this.currentPopup = id;
localStorage.setItem(`popup_shown_${id}`, Date.now().toString());
this.shown.add(id);
localStorage.setItem('shown_popups', JSON.stringify([...this.shown]));
document.getElementById(`popup-${id}`)?.classList.add('popup--visible');
document.body.classList.add('popup-open');
}
close(id: string): void {
this.currentPopup = null;
document.getElementById(`popup-${id}`)?.classList.remove('popup--visible');
document.body.classList.remove('popup-open');
}
}
const popups = new PopupManager();
function showPopup(id: string) { popups.show(id); }
Трекінг показів і конверсій
Кожний поп-ап повинен фіксувати impressions, clicks і closes:
document.querySelectorAll('[data-popup]').forEach(popup => {
const id = popup.getAttribute('data-popup')!;
// Impression при відкритті
const observer = new MutationObserver(() => {
if (popup.classList.contains('popup--visible')) {
gtag('event', 'popup_impression', { popup_id: id });
}
});
observer.observe(popup, { attributes: true, attributeFilter: ['class'] });
// Клік по CTA
popup.querySelector('[data-cta]')?.addEventListener('click', () => {
gtag('event', 'popup_cta_click', { popup_id: id });
});
// Закриття
popup.querySelector('[data-close]')?.addEventListener('click', () => {
gtag('event', 'popup_close', { popup_id: id });
popups.close(id);
});
});
Графік
Реалізація одного поп-апа з exit intent і таймером: 3-5 годин. Повний менеджер з 4-6 тригерів, антиспам-логіки та трекінгу: 1-2 дні. Інтеграція з ESP для захоплення email прямо з поп-апа: ще 2-4 години.







