Реализация триггерных 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 часа.







