Реализация Scroll-triggered Popup на сайте

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация Scroll-triggered Popup на сайте
Простая
~1 рабочий день
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

Реализация Scroll-triggered Popup на сайте

Scroll-triggered popup появляется когда пользователь прокрутил определённый процент страницы. Логика: если человек дочитал до середины статьи или страницы продукта — он вовлечён, и предложение подписаться или получить скидку попадает в нужный момент.

Реализация с Intersection Observer

IntersectionObserver эффективнее scroll-события: не блокирует основной поток, не требует requestAnimationFrame.

// scroll-popup.ts
interface ScrollPopupConfig {
  triggerPercent?: number;    // % прокрутки страницы (0-100)
  triggerElement?: string;    // CSS-селектор элемента-триггера
  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);

    // Позиционируем маркер на triggerPercent высоты документа
    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 } // 50% элемента видно
    );
    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.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>
  );
}

Сроки

День-два с учётом адаптивного дизайна, тестирования на реальных устройствах и настройки cooldown-логики. Если нужна аналитика конверсий (GA4 + собственный трекинг) — добавьте полдня.