Marketing & Social

Free Exit Intent Popup Widget

Catch visitors with one last offer right before they leave your site.

A lightweight modal that appears the moment a visitor moves their cursor toward the browser tab to leave. It shows once per session and can be delayed so it only targets engaged visitors. Install it with a single script tag anywhere on your site.

Live demo

Embed this widget

Paste either snippet into your page — a Custom HTML block on WordPress, Shopify, or Squarespace works too. Both snippets update live as you customize, so what you copy is exactly what you configured.

Hosted (recommended)html
<script src="https://hayneslab.dev/widgets/exit-intent-popup.js" data-hl-title="Wait — don't leave empty-handed!" data-hl-message="Get our free website checklist and turn more visitors into customers." data-hl-button-text="Send me the checklist" async></script>
<!-- Free Exit Intent Popup widget by haynes lab — https://hayneslab.dev/tools/widgets/exit-intent-popup -->
Inline (self-contained)html
<!-- Free Exit Intent Popup widget by haynes lab — https://hayneslab.dev/tools/widgets/exit-intent-popup -->
<script data-hl-title="Wait — don't leave empty-handed!" data-hl-message="Get our free website checklist and turn more visitors into customers." data-hl-button-text="Send me the checklist">
/*!
 * haynes lab — Exit Intent Popup widget (free)
 * Docs & embed code: https://hayneslab.dev/tools/widgets/exit-intent-popup
 */
(function () {
  'use strict';

  var PREFIX = 'hlw-exit-intent-popup';
  var STYLE_ID = 'hlw-exit-intent-popup-styles';

  function findScript() {
    return document.currentScript ||
      document.querySelector('script[src*="widgets/exit-intent-popup.js"]');
  }

  function injectStyles() {
    if (document.getElementById(STYLE_ID)) return;
    var css =
      '.' + PREFIX + ', .' + PREFIX + ' * { box-sizing: border-box; }' +
      '.' + PREFIX + ' { font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;' +
      ' font-size: 16px; line-height: 1.5; color: #1f2937; }' +
      '.' + PREFIX + '__overlay { position: fixed; inset: 0; z-index: 99999; display: flex;' +
      ' align-items: center; justify-content: center; padding: 20px;' +
      ' background: rgba(17, 24, 39, 0.6); }' +
      '.' + PREFIX + '__overlay--staged { position: absolute; z-index: 10; }' +
      '.' + PREFIX + '__dialog { position: relative; width: 100%; max-width: 440px; padding: 36px 32px 44px;' +
      ' background: #ffffff; border: 1px solid #e5e7eb; border-radius: 12px; text-align: center;' +
      ' box-shadow: 0 20px 50px rgba(0, 0, 0, 0.25); }' +
      '.' + PREFIX + '__close { position: absolute; top: 10px; right: 10px; width: 32px; height: 32px;' +
      ' border: 0; border-radius: 50%; background: transparent; color: #9ca3af; font-size: 20px;' +
      ' line-height: 1; cursor: pointer; display: flex; align-items: center; justify-content: center; padding: 0; }' +
      '.' + PREFIX + '__close:hover { background: #f3f4f6; color: #4b5563; }' +
      '.' + PREFIX + '__close:focus-visible { outline: 2px solid var(--hlw-accent); outline-offset: 2px; }' +
      '.' + PREFIX + '__title { margin: 0 0 10px; font-size: 22px; font-weight: 700; color: #111827; }' +
      '.' + PREFIX + '__message { margin: 0 0 22px; font-size: 15px; color: #4b5563; }' +
      '.' + PREFIX + '__cta { display: inline-block; padding: 12px 26px; border: 0; border-radius: 8px;' +
      ' background: var(--hlw-accent); color: #ffffff; font-size: 15px; font-weight: 600;' +
      ' text-decoration: none; cursor: pointer; }' +
      '.' + PREFIX + '__cta:hover { filter: brightness(0.92); }' +
      '.' + PREFIX + '__cta:focus-visible { outline: 2px solid var(--hlw-accent); outline-offset: 2px; }' +
      '.' + PREFIX + '__preview { display: inline-flex; align-items: center; gap: 6px; padding: 8px 16px;' +
      ' border: 1px dashed #d1d5db; border-radius: 8px; background: #f9fafb; color: #4b5563;' +
      ' font-size: 13px; cursor: pointer; }' +
      '.' + PREFIX + '__preview:hover { border-color: var(--hlw-accent); color: var(--hlw-accent); }' +
      '.' + PREFIX + '__preview:focus-visible { outline: 2px solid var(--hlw-accent); outline-offset: 2px; }' +
      '.' + PREFIX + '__credit { position: absolute; right: 10px; bottom: 8px; font-size: 11px;' +
      ' display: inline-flex; align-items: center; gap: 4px; color: #9ca3af; text-decoration: none; }' +
      '.' + PREFIX + '__credit svg { display: block; }' +
      '.' + PREFIX + '__credit:hover { color: #6b7280; text-decoration: underline; }' +
      '@media (max-width: 560px) { .' + PREFIX + '__dialog { padding: 28px 20px 42px; }' +
      ' .' + PREFIX + '__title { font-size: 19px; } }';
    var style = document.createElement('style');
    style.id = STYLE_ID;
    style.textContent = css;
    document.head.appendChild(style);
  }

  function el(tag, className, text) {
    var node = document.createElement(tag);
    if (className) node.className = className;
    if (text != null) node.textContent = text;
    return node;
  }

  function logoSvg() {
    var svgNS = 'http://www.w3.org/2000/svg';
    var svg = document.createElementNS(svgNS, 'svg');
    svg.setAttribute('viewBox', '0 0 32 32');
    svg.setAttribute('width', '13');
    svg.setAttribute('height', '13');
    svg.setAttribute('aria-hidden', 'true');
    svg.setAttribute('focusable', 'false');
    function circle(cx, cy, r, attrs) {
      var c = document.createElementNS(svgNS, 'circle');
      c.setAttribute('cx', cx);
      c.setAttribute('cy', cy);
      c.setAttribute('r', r);
      for (var key in attrs) c.setAttribute(key, attrs[key]);
      svg.appendChild(c);
    }
    circle(16, 16, 16, { fill: '#0f0a0a' });
    circle(16, 16, 12, { fill: 'none', stroke: '#fff5f5', 'stroke-width': 1, opacity: 0.3 });
    circle(16, 16, 9, { fill: 'none', stroke: '#fff5f5', 'stroke-width': 1, opacity: 0.5 });
    circle(16, 16, 6, { fill: 'none', stroke: '#fff5f5', 'stroke-width': 1.2, opacity: 0.9 });
    circle(16, 16, 3, { fill: '#06b6d4' });
    circle(24.5, 7.5, 2, { fill: '#f97316' });
    circle(7, 16, 1.8, { fill: '#f97316' });
    circle(20, 20, 1.5, { fill: '#f97316' });
    return svg;
  }

  function build(script) {
    var title = script.getAttribute('data-hl-title') || 'Before you go…';
    var message = script.getAttribute('data-hl-message') ||
      'Join our newsletter and get 10% off your first order.';
    var buttonText = script.getAttribute('data-hl-button-text') || 'Claim my discount';
    var link = script.getAttribute('data-hl-link') || '#';
    var delay = parseInt(script.getAttribute('data-hl-delay'), 10);
    if (isNaN(delay) || delay < 0) delay = 5000;
    var accent = script.getAttribute('data-hl-accent') || '#f97316';

    injectStyles();

    var stage = script.closest ? script.closest('.widget-demo__stage') : null;
    var storageKey = 'hlw-exit-intent-popup-shown:' + title;

    var overlay = el('div', PREFIX + ' ' + PREFIX + '__overlay' + (stage ? ' ' + PREFIX + '__overlay--staged' : ''));
    overlay.style.setProperty('--hlw-accent', accent);
    overlay.setAttribute('hidden', '');
    overlay.style.display = 'none';

    var dialog = el('div', PREFIX + '__dialog');
    dialog.setAttribute('role', 'dialog');
    dialog.setAttribute('aria-modal', 'true');
    dialog.setAttribute('aria-label', title);

    var close = el('button', PREFIX + '__close', '×');
    close.type = 'button';
    close.setAttribute('aria-label', 'Close popup');

    dialog.appendChild(close);
    dialog.appendChild(el('h2', PREFIX + '__title', title));
    dialog.appendChild(el('p', PREFIX + '__message', message));

    var cta = el('a', PREFIX + '__cta', buttonText);
    cta.href = link;
    dialog.appendChild(cta);

    var credit = el('a', PREFIX + '__credit');
    credit.appendChild(logoSvg());
    credit.appendChild(document.createTextNode('Powered by haynes lab'));
    credit.href = 'https://hayneslab.dev/tools/widgets/exit-intent-popup';
    credit.target = '_blank';
    credit.rel = 'noopener';
    dialog.appendChild(credit);

    overlay.appendChild(dialog);

    function hide() {
      overlay.style.display = 'none';
      overlay.setAttribute('hidden', '');
      document.removeEventListener('keydown', onKeydown);
    }
    function onKeydown(e) {
      if (e.key === 'Escape') hide();
    }
    function show(markShown) {
      if (markShown) {
        try { sessionStorage.setItem(storageKey, '1'); } catch (e) { /* storage unavailable */ }
      }
      overlay.removeAttribute('hidden');
      overlay.style.display = 'flex';
      document.addEventListener('keydown', onKeydown);
      close.focus();
    }

    close.addEventListener('click', hide);
    overlay.addEventListener('click', function (e) {
      if (e.target === overlay) hide();
    });

    if (stage) {
      // Landing-page demo: never auto-trigger, offer a preview button instead.
      stage.appendChild(overlay);
      var preview = el('button', PREFIX + ' ' + PREFIX + '__preview', 'Preview popup');
      preview.type = 'button';
      preview.style.setProperty('--hlw-accent', accent);
      preview.addEventListener('click', function () { show(false); });
      stage.appendChild(preview);
      return;
    }

    // Real page: arm exit-intent once the visitor has been here data-hl-delay ms.
    document.body.appendChild(overlay);
    var armed = false;
    try { armed = sessionStorage.getItem(storageKey) === '1'; } catch (e) { /* ignore */ }
    if (armed) return;

    setTimeout(function () {
      document.addEventListener('mouseleave', function onLeave(e) {
        if (e.clientY > 0) return; // only when leaving toward the top of the viewport
        document.removeEventListener('mouseleave', onLeave);
        show(true);
      });
    }, delay);
  }

  function init() {
    var script = findScript();
    if (!script) {
      console.warn('[haynes lab exit-intent-popup] Could not locate its own script tag. Nothing rendered.');
      return;
    }
    build(script);
  }

  if (document.body) {
    init();
  } else {
    document.addEventListener('DOMContentLoaded', init);
  }
})();

</script>

Configuration

Every option is an attribute on the script tag — the same attributes the customizer above exposes. All attributes are optional unless marked required — defaults apply when omitted.

AttributeDefaultDescription
data-hl-title"Before you go…"The heading shown at the top of the popup.
data-hl-message"Join our newsletter and get 10% off your first order."The body text explaining your last-chance offer.
data-hl-button-text"Claim my discount"The label on the call-to-action button.
data-hl-link"#"The URL the call-to-action button points to.
data-hl-delay"5000"Milliseconds a visitor must stay before the popup can trigger.
data-hl-accent"#f97316"Color of the call-to-action button and focus outlines.

Frequently asked questions

How do I install the Exit Intent Popup?

Paste the script tag anywhere before the closing </body> tag on your site. The popup stays hidden until a visitor who has been on the page at least data-hl-delay milliseconds moves their cursor toward the top of the browser window, then it appears as a centered modal.

Can I change the text, link, and colors?

Yes — set data-hl-title, data-hl-message, data-hl-button-text, and data-hl-link on the script tag, and use data-hl-accent to change the button color. data-hl-delay controls how long a visitor must stay before the popup is armed.

How often does the popup show to the same visitor?

Once per browser session. After it appears, a sessionStorage flag prevents it from showing again until the visitor closes and reopens their browser.

Does it work on WordPress, Shopify, or Squarespace?

Yes — it is a single vanilla JavaScript file with no dependencies. Paste the snippet into any platform that lets you add custom scripts or edit your theme, including WordPress, Shopify, Squarespace, Wix, and plain HTML sites.

More free widgets

Need a custom widget?

We build custom widgets, integrations, and whole products. Tell us what your site needs and we'll scope it.

Get in Touch