פופ-אפ לכוונת יציאה: שימור משתמשים ואיסוף דוא"ל
אתה מאבד עד 70% ממבקרי האתר מבלי לדעת למה הם עוזבים. הם פשוט סוגרים את הטאב — ללא משוב. פופ-אפ לכוונת יציאה הוא ההזדמנות האחרונה שלך ללכוד דוא"ל על ידי הצעת הנחה או תוכן בעל ערך. אנו מיישמים גלאי ב-TypeScript ורכיב React עם אנליטיקה. לפי נתוני הפרויקטים שלנו, פופ-אפ כזה מגדיל את ההמרה לאיסוף דוא"ל ב-20–40% בהשוואה לפופ-אפים סטנדרטיים מבוססי טיימר. חשוב לציין, אנו משתמשים בקירור של 24 שעות כדי לא להטריד את המשתמש. במאמר זה, נסקור את היישום הטכני, טעויות נפוצות, וכיצד למדוד תוצאות. אם אתה רוצה פתרון מהיר, הזמן פיתוח של הרכיב עם אנליטיקה ובדיקות A/B — נעריך את הפרויקט תוך יום עסקים אחד.
למה פופ-אפים סטנדרטיים לא עובדים
פופ-אפים טיפוסיים מופיעים אחרי N שניות או בגלילה — המשתמש עדיין לא מוכן לעזוב, אבל הם מופרעים. זה גורם לעצבנות וסגירה ללא המרה. כוונת יציאה מגיבה לכוונה לעזוב: תנועת עכבר לכיוון כפתור סגירת הטאב (במחשב) או מעבר למצב רקע (בנייד). זה קונטקסטואלי ופחות פולשני. לדוגמה, בפרויקט מסחר אלקטרוני עם 50,000 מבקרים חודשיים, השגנו 2,000 מנויים בחודש הראשון לאחר היישום — פי 3 יותר מאשר עם פופ-אפ טיימר סטנדרטי.
השוואה: פופ-אפ סטנדרטי לעומת פופ-אפ לכוונת יציאה
| קריטריון | פופ-אפ סטנדרטי | פופ-אפ לכוונת יציאה |
|---|---|---|
| זמן תצוגה | אחרי N שניות | כשהמשתמש מתכוון לעזוב |
| המרת דוא"ל | 2–5% | 10–15% |
| שיעור סגירה | 70–90% | 40–60% |
| השפעה על חוויית משתמש | מעצבן | מינימלי עם קירור |
פופ-אפ לכוונת יציאה מספק המרת איסוף דוא"ל גבוהה פי 2–3 עם פחות הטרדה למשתמש.
איך לזהות כוונת יציאה בנייד
במחשב, אנו מאזינים ל-mouseleave עם סף קואורדינטת Y (בדרך כלל 20px מהחלק העליון). בנייד, אנו משתמשים ב-visibilitychange וב-pagehide. חשוב לבדוק זמן מינימלי בדף (לפחות 3 שניות) וקירור (אנו מגדירים 24 שעות באמצעות sessionStorage).
// 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();
}
// Мобильный 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);
},
};
} רכיב הפופ-אפ (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,
}),
});
}// 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(); } // Мобильный 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); }, }; } הוא קריטי עבור // 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> ); } באירועים שעשויים להישלח בעת סגירת הטאב — בלעדיו הדפדפן עלול לבטל את הבקשה.
איך להימנע מהטרדת משתמשים
קירור הוא הדרך היחידה למנוע מהפופ-אפ להפוך למטרד. אנו משתמשים במרווח של 24 שעות, ושומרים את חותמת הזמן ב-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, }), }); } . אם המשתמש כבר המיר, הקירור מתאפס לצמיתות. בנוסף, אנו מגדירים עיכוב מינימלי לפני התצוגה הראשונה — לפחות 3 שניות לאחר טעינת הדף. בנייד, אנו מבטלים את keepalive: true כך שנגיעה מקרית לא תפעיל את הפופ-אפ. אמצעים אלה מפחיתים את שיעור הסגירה ל-40–60% ואינם פוגעים במדדים התנהגותיים.
טעויות יישום נפוצות
| טעות | פתרון נכון |
|---|---|
| פופ-אפ בכל ביקור | קירור של 24 שעות באמצעות fetch |
הצגה בנייד באמצעות sessionStorage |
השתמש ב-mouseleave וב-sessionStorage |
חוסר ב-polyfill עבור mouseleave |
בדוק תמיכה – visibilitychange ב-Chrome 37+, Firefox 98+, Safari 15.4+ |
| אין אנליטיקה | הוסף אירועי pagehide, <dialog>, HTMLDialogElement |
ציר זמן
פופ-אפ בסיסי עם גלאי ואנליטיקה: מיומיים עד חמישה ימים. בדיקות A/B של מספר גרסאות עם בחירה אוטומטית של הזוכה: עוד יומיים עד שלושה ימים.
מה כלול
- עיצוב תרחיש (טקסט, עיצוב, תנאי תצוגה)
- פיתוח הגלאי ב-TypeScript (תואם דפדפנים)
- שילוב רכיב React עם אפשרויות התאמה אישית
- הגדרת אנליטיקה (GA4 + מותאם אישית)
- תיעוד לשימוש ותחזוקה
קבל ייעוץ על אינטגרציה — נבצע ביקורת המרות באתר שלך ונציע את פופ-אפ כוונת היציאה האופטימלי. הזמן פיתוח של הרכיב עם אנליטיקה ובדיקות A/B. נעריך את הפרויקט תוך יום עסקים אחד.
למה להשתמש בכוונת יציאה?
זו הדרך הלא-אגרסיבית היחידה ליירט משתמש שעוזב. לפי הנתונים שלנו, יישום פופ-אפ כזה מגדיל את ההמרה לאיסוף דוא"ל ב-20–40%. זה לא פוגע בחוויית המשתמש אם מוגדרים קירור והצעה רלוונטית. אנו עובדים עם פופ-אפים לכוונת יציאה למעלה מ-5 שנים ויישמנו יותר מ-50 פרויקטים במסחר אלקטרוני, SaaS ומדיה. אנו מבטיחים פרטיות של הנתונים שנאספו ועמידה בדרישות GDPR.







