Реалізація Time-delayed Popup на сайті
Уявіть: ви запускаєте рекламну кампанію, ліди потрібні вже вчора, але спливаюче вікно з'являється через 2 секунди після завантаження — користувач ще не побачив контент і закриває його. Конверсія падає, бюджет злито. Time-delayed popup вирішує цю проблему: показуємо пропозицію рівно в той момент, коли відвідувач готовий її сприйняти. При цьому не дратуємо користувача — кулдаун і A/B-тестування дозволяють знайти ідеальний баланс.
Проблеми, які ми вирішуємо
- Передчасний показ попапа: Якщо вікно з'являється занадто рано, користувач ще не встиг ознайомитися з контентом і закриває його — конверсія падає.
- Відсутність кулдауну: Без обмеження частоти показу той самий відвідувач бачить попап при кожному завантаженні сторінки, що викликає роздратування і збільшує відмови.
- Неоптимальний вибір пропозиції: Без A/B-тестування неможливо визначити, який варіант контенту (знижка, лід-магніт, колбек) працює краще для вашої аудиторії.
- Відсутність трекінгу: Без аналітики неможливо оцінити ефективність попапа і прийняти обґрунтовані рішення.
Як ми це робимо (доказ експертності)
Наш підхід базується на використанні нативного <dialog>, TypeScript та React. Ми реалізували цю схему на багатьох проєктах, і вона показала стабільне зростання конверсії на 15–30% порівняно з миттєвим показом.
Базова реалізація
// time-popup.ts interface TimePopupConfig { delayMs: number; cooldownKey: string; // ключ в localStorage cooldownMs?: number; // 0 = показати один раз назавжди onShow: () => void; } export function schedulePopup(config: TimePopupConfig): () => void { const { delayMs, cooldownKey, cooldownMs, onShow } = config; // Перевіряємо, чи потрібно показувати const lastShown = localStorage.getItem(cooldownKey); if (lastShown) { if (!cooldownMs) return () => {}; // показали один раз, більше не треба if (Date.now() - Number(lastShown) < cooldownMs) return () => {}; } const timer = setTimeout(() => { // Додаткова перевірка: користувач все ще на сторінці if (document.hidden) return; localStorage.setItem(cooldownKey, String(Date.now())); onShow(); }, delayMs); return () => clearTimeout(timer); } Попап з кількома варіантами контенту (A/B)
// DelayedPopup.tsx import { useEffect, useRef, useState } from 'react'; import { schedulePopup } from './time-popup'; type PopupVariant = 'discount' | 'lead-magnet' | 'callback'; const VARIANTS: Record<PopupVariant, { headline: string; body: string; cta: string; }> = { discount: { headline: 'Знижка 10% на перше замовлення', body: 'Введіть email — надішлемо промокод негайно.', cta: 'Отримати знижку', }, 'lead-magnet': { headline: 'Безкоштовний чеклист', body: '15 пунктів, які ми перевіряємо на кожному проєкті.', cta: 'Завантажити PDF', }, callback: { headline: 'Залишились питання?', body: 'Залиште номер — передзвонимо протягом 15 хвилин.', cta: 'Чекаю дзвінка', }, }; // Простий детермінований A/B: ділимо за останньою цифрою дати function pickVariant(): PopupVariant { const bucket = new Date().getDate() % 3; return (['discount', 'lead-magnet', 'callback'] as PopupVariant[])[bucket]; } export function DelayedPopup() { const [open, setOpen] = useState(false); const [variant] = useState<PopupVariant>(pickVariant); const [value, setValue] = useState(''); const [submitted, setSubmitted] = useState(false); const dialogRef = useRef<HTMLDialogElement>(null); useEffect(() => { const cleanup = schedulePopup({ delayMs: 7000, cooldownKey: 'delayed_popup_v2', cooldownMs: 3 * 24 * 60 * 60 * 1000, // раз на 3 дні onShow: () => { setOpen(true); // трекінг показу window.gtag?.('event', 'popup_shown', { popup_variant: variant, page_path: location.pathname, }); }, }); return cleanup; }, [variant]); useEffect(() => { if (open) { dialogRef.current?.showModal(); } else { dialogRef.current?.close(); } }, [open]); async function handleSubmit() { if (!value.trim()) return; await fetch('/api/popup-lead', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ value, variant, source: 'time_delayed_popup', pageUrl: location.href, }), }); window.gtag?.('event', 'popup_converted', { popup_variant: variant }); setSubmitted(true); setTimeout(() => setOpen(false), 2500); } const content = VARIANTS[variant]; const inputType = variant === 'callback' ? 'tel' : 'email'; const placeholder = variant === 'callback' ? '+380 (99) 000-00-00' : '[email protected]'; return ( <dialog ref={dialogRef} onClose={() => setOpen(false)} className="max-w-sm w-full rounded-2xl p-0 shadow-2xl backdrop:bg-black/50 animate-in fade-in slide-in-from-bottom-4" > <div className="relative p-7"> <button onClick={() => setOpen(false)} aria-label="Закрити" className="absolute top-3 right-3 w-7 h-7 flex items-center justify-center rounded-full hover:bg-gray-100 text-gray-400" > ✕ </button> {submitted ? ( <div className="py-4 text-center"> <div className="text-3xl mb-2">✅</div> <p className="font-semibold text-gray-800">Готово, чекайте!</p> </div> ) : ( <> <h2 className="text-xl font-bold text-gray-900 mb-1">{content.headline}</h2> <p className="text-sm text-gray-500 mb-5">{content.body}</p> <div className="flex gap-2"> <input type={inputType} value={value} onChange={e => setValue(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleSubmit()} placeholder={placeholder} autoFocus className="flex-1 border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" /> <button onClick={handleSubmit} className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg whitespace-nowrap" > {content.cta} </button> </div> <p className="mt-3 text-xs text-gray-400"> Не будемо турбувати частіше одного разу на тиждень </p> </> )} </div> </dialog> ); } A/B-тестування для максимальної ефективності popup
A/B-тестування дозволяє порівняти кілька варіантів контенту — знижка, лід-магніт, зворотний дзвінок — і обрати той, що приносить більше конверсій. У нашій реалізації варіанти чергуються детерміновано: кожен день показується один із трьох варіантів. Це спрощує аналіз і виключає вплив дня тижня. Результати тесту оцінюються за метриками: покази, конверсії, CTR. Оптимальна тривалість тесту — 7–14 днів.
Процес оцінки та роботи
- Аналітика — вивчаємо поточну поведінку користувачів через Google Analytics 4 і карти кліків.
- Проєктування — обираємо затримку, варіанти контенту, кулдаун. Робимо прототип у Figma.
-
Реалізація — використовуємо нативні
<dialog>(підтримка IE не потрібна), код на TypeScript + React. Інтегруємо трекінг. Детальніше про<dialog>читайте в MDN. - A/B-тест — запускаємо тест на 7–14 днів, порівнюємо конверсію.
- Деплой та оптимізація — фіксуємо найкращий варіант, доналаштовуємо під Core Web Vitals.
Деталі версіонування localStorage
Ключі кулдауну автоматично включають версію попапа (наприклад, `popup_v2_lead_magnet`). Це дозволяє скинути покази при оновленні контенту, просто змінивши номер версії в конфігу.Що входить у роботу
| Компонент | Опис |
|---|---|
| Вихідний код | TypeScript/React компонент з конфігурацією |
| Документація | Інструкція зі зміни контенту та затримки |
| Інтеграція трекінгу | Події в Google Analytics 4 |
| Налаштування кулдауну | localStorage з версіонуванням |
| A/B-тестування | Детермінований розподіл по днях |
| Підтримка | 1 місяць базової підтримки після деплою |
Орієнтири по термінах
Термін реалізації — від 1 до 3 днів залежно від складності дизайну та необхідності A/B-тесту. Вартість визначається після аудиту.
Чек-лист типових помилок
- Занадто коротка затримка (< 3 секунд) — користувач не встигає оцінити контент.
- Відсутність кулдауну — попап дратує при кожному візиті.
- Один варіант контенту — без тестування неможливо дізнатися, що працює краще.
- Ігнорування мобільних пристроїв — на мобільних
Досвід компанії
Понад 10 років на ринку веб-розробки. Реалізували 40+ проєктів для e-commerce, SaaS та корпоративних сайтів. Гарантуємо чистий код, безпеку (localStorage не зберігає чутливі дані) та повну інтеграцію з вашою CRM.
Замовте впровадження time-delayed popup — зв'яжіться з нами для оцінки вашого проєкту.







