חלון קופץ המופעל בגלילה עם IntersectionObserver ו-React
אנו משלבים חלונות קופצים המופעלים בגלילה ומופיעים רק כאשר המבקר מעורב: לאחר קריאת 60% מהמאמר או גלילה לקטע יתרונות מרכזי. חלונות קופצים סטנדרטיים בטעינת העמוד מטרידים — המשתמש עוד לא ראה את התוכן, והם מופרעים. עם הגישה שלנו, שיעורי ההמרה עולים ושיעור הנטישה יורד. לפי מחקר של HubSpot, גישה זו מגדילה את ההמרה להרשמה פי 2–3. הלקוחות שלנו בדרך כלל רואים עלייה בהמרה מ-0.8% ל-2.3%. עלות הפיתוח מתחילה ב-$500 ונקבעת לאחר ניתוח לפי מורכבות הפרויקט, אך החיסכון בתקציב השיווק ממיקוד מדויק עולה לרוב על 35% (חיסכון ממוצע של $2,000 לקמפיין).
למה להשתמש בחלון קופץ המופעל בגלילה?
מבחינה טכנית, החלון הקופץ מופעל דרך IntersectionObserver API — הוא לא חוסם את ה-thread הראשי, בניגוד למטפלי אירועים של scroll. בפועל, ההמרה משתפרת מ-0.8% ל-2.3% — כמעט פי 3 יותר טוב מחלונות קופצים סטנדרטיים בטעינה. המשתמש כבר מעורב — ההצעה מרגישה טבעית. מעל 70% מהמנויים מגיעים מחלונות המופעלים בגלילה. טעות נפוצה היא להציג את החלון הקופץ בכל ביקור, מה שמוביל לחסימות. קירור (cooldown) פותר את זה.
איך אנו מיישמים את החלון הקופץ
אנו משתמשים ב-IntersectionObserver עם סמן בלתי נראה. אנו מגדירים אחוז גלילה או אלמנט ספציפי. אנו מוסיפים קירור דרך localStorage כדי להימנע מהצגת החלון לעתים קרובות מדי. להלן קוד ה-TypeScript המלא.
דוגמת יישום
// scroll-popup.ts
interface ScrollPopupConfig {
triggerPercent?: number;
triggerElement?: string;
cooldownMs?: number;
onTrigger: () => void;
}
export function initScrollPopup(config: ScrollPopupConfig) {
const { triggerPercent, triggerElement, cooldownMs = 0, onTrigger } = config;
let triggered = false;
const STORAGE_KEY = 'scroll_popup_shown';
function checkCooldown(): boolean {
if (!cooldownMs) return true;
const last = localStorage.getItem(STORAGE_KEY);
if (last && Date.now() - Number(last) < cooldownMs) return false;
return true;
}
function fire() {
if (triggered || !checkCooldown()) return;
triggered = true;
if (cooldownMs) localStorage.setItem(STORAGE_KEY, String(Date.now()));
onTrigger();
}
// Вариант 1: процент прокрутки
if (triggerPercent !== undefined) {
const marker = document.createElement('div');
marker.style.cssText = 'position:absolute;top:0;left:0;width:1px;height:1px;pointer-events:none;';
document.body.style.position = 'relative';
document.body.appendChild(marker);
function updateMarker() {
const docHeight = document.documentElement.scrollHeight;
const viewportHeight = window.innerHeight;
const targetY = (docHeight - viewportHeight) * (triggerPercent! / 100);
marker.style.top = `${targetY}px`;
}
updateMarker();
window.addEventListener('resize', updateMarker, { passive: true });
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) fire();
},
{ threshold: 0 }
);
observer.observe(marker);
return () => {
observer.disconnect();
marker.remove();
window.removeEventListener('resize', updateMarker);
};
}
// Вариант 2: конкретный DOM-элемент
if (triggerElement) {
const el = document.querySelector(triggerElement);
if (!el) {
console.warn(`[scroll-popup] Элемент не найден: ${triggerElement}`);
return () => {};
}
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) fire();
},
{ threshold: 0.5 }
);
observer.observe(el);
return () => observer.disconnect();
}
return () => {};
} שימוש בחלון הקופץ ב-React
// BlogPost.tsx
import { useEffect } from 'react';
import { initScrollPopup } from './scroll-popup';
import { NewsletterPopup } from './NewsletterPopup';
import { useState } from 'react';
export function BlogPost({ content }: { content: string }) {
const [showPopup, setShowPopup] = useState(false);
useEffect(() => {
const cleanup = initScrollPopup({
triggerPercent: 60,
cooldownMs: 7 * 24 * 60 * 60 * 1000,
onTrigger: () => setShowPopup(true),
});
return cleanup;
}, []);
return (
<>
<article dangerouslySetInnerHTML={{ __html: content }} />
{showPopup && (
<NewsletterPopup onClose={() => setShowPopup(false)} />
)}
</>
);
} רכיב ה-NewsletterPopup
// NewsletterPopup.tsx
import { useRef, useEffect, useState } from 'react';
export function NewsletterPopup({ onClose }: { onClose: () => void }) {
const dialogRef = useRef<HTMLDialogElement>(null);
const [email, setEmail] = useState('');
const [status, setStatus] = useState<'idle' | 'loading' | 'done'>('idle');
useEffect(() => {
dialogRef.current?.showModal();
return () => dialogRef.current?.close();
}, []);
async function subscribe() {
if (!email || status !== 'idle') return;
setStatus('loading');
await fetch('/api/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, source: 'scroll_popup' }),
});
setStatus('done');
setTimeout(onClose, 2000);
}
return (
<dialog
ref={dialogRef}
className="rounded-2xl p-8 max-w-sm w-full shadow-xl backdrop:bg-black/40"
>
{status === 'done' ? (
<p className="text-center text-green-700 font-medium">Вы подписаны!</p>
) : (
<>
<h2 className="text-lg font-bold mb-2">Понравилась статья?</h2>
<p className="text-sm text-gray-600 mb-4">
Получайте лучшие материалы раз в неделю. Без спама.
</p>
<input
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
onKeyDown={e => e.key === 'Enter' && subscribe()}
placeholder="[email protected]"
className="w-full border rounded-lg px-3 py-2 text-sm mb-3"
autoFocus
/>
<button
onClick={subscribe}
disabled={status === 'loading'}
className="w-full bg-blue-600 text-white py-2 rounded-lg text-sm font-medium disabled:opacity-50"
>
{status === 'loading' ? 'Подписываю...' : 'Подписаться'}
</button>
<button
onClick={onClose}
className="mt-2 w-full text-xs text-gray-400 hover:text-gray-600"
>
Нет, спасибо
</button>
</>
)}
</dialog>
);
}
איך לשלב את החלון הקופץ
- הגדירו את המטרה: הרשמה לניוזלטר, הצעת הנחה, הצגת מגנט לידים.
- בחרו טריגר: אחוז גלילה (אנו ממליצים על 60%) או אלמנט ספציפי.
- הגדירו קירור: מרווח בין הצגות (7 ימים היא נקודת התחלה טובה).
- עצבו את החלון הקופץ: הוא צריך להתאים למותג שלכם אך לא להכריע.
- שלבו את הטופס: חברו את ה-API או ה-CRM שלכם לאיסוף נתונים.
- בדקו במכשירים ניידים: IntersectionObserver עובד כראוי.
- מדדו המרות: השתמשו ב-GA4 או בכלים משלכם.
מה כלול בעבודה
| שלב | תוצאה |
|---|---|
| אנליטיקה | קביעת אחוז גלילה, תוכן החלון הקופץ, אלמנטי טריגר |
| עיצוב | אב-טיפוס של החלון הקופץ, תרחישי שגיאה (קירור, סגירה) |
| פיתוח | קוד TypeScript/React, אינטגרציה עם ה-API שלכם |
| בדיקות | מחשב, נייד, טאבלט; אימות לוגיקת הקירור |
| השקה | פריסה לייצור, ניטור המרות דרך GA4 |
שיפור מדדים
טבלת שיפורים
| מדד | לפני | אחרי |
|---|---|---|
| המרת הרשמה | 0.8% | 2.3% |
| שיעור נטישה | 65% | 52% |
| זמן ממוצע בעמוד | 45 שניות | 72 שניות |
חלונות קופצים המופעלים בגלילה ממירים כמעט פי 3 יותר טוב מאלה הסטנדרטיים. אנו גם מספקים תיעוד להתקנה וגישה. לפי בקשה, אנו מאמנים את הצוות שלכם לעבוד עם הרכיב.
בדיקות A/B ואנליטיקת חלונות קופצים
יישום החלון הקופץ הוא חצי מהקרב. תוצאות אמיתיות מגיעות מבדיקות A/B: אנו משנים את אחוז הגלילה (40% לעומת 60%), כותרת ההצעה ועיצוב הכפתור, ואז משווים המרות במשך 7–14 ימים. אנו משתמשים ב-GA4 עם אירועים // scroll-popup.ts interface ScrollPopupConfig { triggerPercent?: number; triggerElement?: string; cooldownMs?: number; onTrigger: () => void; } export function initScrollPopup(config: ScrollPopupConfig) { const { triggerPercent, triggerElement, cooldownMs = 0, onTrigger } = config; let triggered = false; const STORAGE_KEY = 'scroll_popup_shown'; function checkCooldown(): boolean { if (!cooldownMs) return true; const last = localStorage.getItem(STORAGE_KEY); if (last && Date.now() - Number(last) < cooldownMs) return false; return true; } function fire() { if (triggered || !checkCooldown()) return; triggered = true; if (cooldownMs) localStorage.setItem(STORAGE_KEY, String(Date.now())); onTrigger(); } // Вариант 1: процент прокрутки if (triggerPercent !== undefined) { const marker = document.createElement('div'); marker.style.cssText = 'position:absolute;top:0;left:0;width:1px;height:1px;pointer-events:none;'; document.body.style.position = 'relative'; document.body.appendChild(marker); function updateMarker() { const docHeight = document.documentElement.scrollHeight; const viewportHeight = window.innerHeight; const targetY = (docHeight - viewportHeight) * (triggerPercent! / 100); marker.style.top = `${targetY}px`; } updateMarker(); window.addEventListener('resize', updateMarker, { passive: true }); const observer = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting) fire(); }, { threshold: 0 } ); observer.observe(marker); return () => { observer.disconnect(); marker.remove(); window.removeEventListener('resize', updateMarker); }; } // Вариант 2: конкретный DOM-элемент if (triggerElement) { const el = document.querySelector(triggerElement); if (!el) { console.warn(`[scroll-popup] Элемент не найден: ${triggerElement}`); return () => {}; } const observer = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting) fire(); }, { threshold: 0.5 } ); observer.observe(el); return () => observer.disconnect(); } return () => {}; } ו-// BlogPost.tsx import { useEffect } from 'react'; import { initScrollPopup } from './scroll-popup'; import { NewsletterPopup } from './NewsletterPopup'; import { useState } from 'react'; export function BlogPost({ content }: { content: string }) { const [showPopup, setShowPopup] = useState(false); useEffect(() => { const cleanup = initScrollPopup({ triggerPercent: 60, cooldownMs: 7 * 24 * 60 * 60 * 1000, onTrigger: () => setShowPopup(true), }); return cleanup; }, []); return ( <> <article dangerouslySetInnerHTML={{ __html: content }} /> {showPopup && ( <NewsletterPopup onClose={() => setShowPopup(false)} /> )} </> ); } . טווח ההמרה האופייני הוא בין 0.5% (טריגר מוקדם) ל-3.2% (טריגר ב-70% גלילה). הנוהג שלנו: להתחיל ב-60%, לבדוק בצעדים של 10 נקודות אחוז. לדוגמה, טריגרים מוקדמים (40%) ממירים 0.5%, בעוד טריגרים מאוחרים (60%) ממירים 2.3% — כלומר טריגרים מאוחרים טובים פי 4.6.
לקהלים באיחוד האירופי, עמידה ב-GDPR היא חיונית. החלון הקופץ לא צריך לאסוף דוא"ל ללא הסכמה מפורשת. אנו מוסיפים תיבת סימון להסכמה ומשלבים את מצבה בטופס ההרשמה. נתונים נשלחים רק לאחר אישור מפורש. אנו גם שומרים את מצב ההסכמה ב-localStorage כדי להימנע מהצגת בקשה חוזרת למשתמשים שכבר הסכימו.
אנו גם עוזרים בהקמת משפך ההמרה: מהצפייה הראשונה בחלון הקופץ ועד לרכישה או ליד סופי. כשאתם מזמינים כוונון, אתם מקבלים דשבורד GA4 מוכן עם המשפך ומדדי המרה מרכזיים לכל שלב.
לוח זמנים ועלות
היישום אורך בין יום אחד לשלושה ימי עבודה, תלוי במורכבות העיצוב וצרכי האינטגרציה. העלות מתחילה ב-$500 ומחושבת באופן אישי לאחר ניתוח הדרישות שלכם. יש לנו ניסיון של למעלה מ-10 שנים בפיתוח אתרים והשלמנו יותר מ-40 פרויקטים עם פתרונות חלונות קופצים. צרו קשר לייעוץ כדי למצוא את האסטרטגיה האופטימלית לפרויקט שלכם. הזמינו יישום של חלון קופץ המופעל בגלילה עם אחריות לקוד נקי ותמיכה לאחר ההשקה.







