Behavior-triggered popup windows on website

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:
Development stages
Latest works
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    822
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    847
  • image_website-sbh_0.png
    Website development for SBH Partners
    999
  • image_website-_0.png
    Website development for Red Pear
    451

Behavior-Triggered Popup Implementation

Triggered popup is a modal that appears not on timer but on specific user action: mouse moving toward tab close, prolonged pause on certain block, scroll to page bottom. Correctly configured popup increases conversion 2-5%, incorrectly configured annoys and drives people away.

Types of Triggers

Main triggers worth implementing:

  • Exit intent — mouse accelerates toward top edge, user about to close tab
  • Scroll depth — scroll reached N% of page
  • Time on page — user spent X seconds on page
  • Inactivity — no mouse movement and clicks for Y seconds
  • Element visibility — specific block entered viewport
  • Click intent — hover over element without clicking

Exit Intent

Most effective trigger for landing pages. Determined by cursor speed and direction:

class ExitIntentDetector {
  private threshold = 10; // pixels from top
  private sensitivity = 50; // px/ms — minimum upward movement speed
  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, negative = upward

    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;
  }
}

// Usage
const detector = new ExitIntentDetector(() => {
  showPopup('exit_offer');
});

On mobile, exit intent doesn't work—no mousemove event. Use back button press via popstate instead:

// Mobile exit: push state to history, catch exit
history.pushState({ popup: true }, '');
window.addEventListener('popstate', (e) => {
  if (!e.state?.popup) {
    showPopup('mobile_exit_offer');
    history.pushState({ popup: true }, '');
  }
});

Scroll Depth Trigger

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);
}

// Popup at 70% scroll
onScrollDepth(70, () => showPopup('mid_page_offer'));

Element Visibility (Intersection Observer)

Show popup when user scrolls to pricing block:

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 } // element visible 50%
  );

  observer.observe(el);
}

onElementVisible('#pricing-section', () => showPopup('pricing_helper'));

Popup State Manager

Critical to not show one popup multiple times and not show several at once:

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; // another already open

    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); }

Tracking Impressions and Conversions

Each popup must record impressions, clicks, and closes:

document.querySelectorAll('[data-popup]').forEach(popup => {
  const id = popup.getAttribute('data-popup')!;

  // Impression on open
  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 click
  popup.querySelector('[data-cta]')?.addEventListener('click', () => {
    gtag('event', 'popup_cta_click', { popup_id: id });
  });

  // Close
  popup.querySelector('[data-close]')?.addEventListener('click', () => {
    gtag('event', 'popup_close', { popup_id: id });
    popups.close(id);
  });
});

Timeline

One popup with exit intent and timer: 3-5 hours. Full manager with 4-6 triggers, anti-spam, tracking: 1-2 days. ESP integration for email capture from popup: 2-4 more hours.