Detecting Exit Intent with Vanilla JavaScript

by Christoph Schiessl on JavaScript

Visitors who find your website via Google will probably never return again. It doesn't matter how good your content is because the Internet has a whole universe of content, and any given website is like a drop in the ocean. To convert one-time visitors to repeat visitors, you have to convince them to opt-in to some communication channel so that you can reach out to them and bring more of your content to their attention.

The best way to do this is to set up a mailing list. You simply ask your visitors for their email addresses so you can message them in the future. There are other ways to do this, too, such as providing an RSS feed, but email is objectively better because it enables two-way communication. Not only can you reach your audience, but your audience can also reply to your emails to get back to you.

One of the most effective ways to ask for email addresses is through modal dialogs. You track how visitors interact with your website, and once a specific condition is met, you show a modal dialog, aka popup. The point is that popups are hard to miss, and you probably have a good chance of getting the visitor's email address if the content they've consumed so far was valuable to them.

Okay, that's enough background information. This article discusses one of the possible triggers you can use to show your popup, which is known as "exit intent" in marketing circles.

What is Exit Intent?

There's nothing you can do once a visitor has closed the browser tab or window containing your website, so you must act while your website is still open. Essentially, you do this by looking for advance indicators that usually precede the closing of your website. Hence, they suggest a visitor's intention to exit your website.

The best indicator is when a visitor's mouse pointer leaves your website's viewport because that's where the buttons to close tabs and windows are located. Technically, you can use JavaScript's mouseout and mouseleave events to detect just that.

Event Handlers for mouseout and mouseleave

It's easy to create event listeners for both events and log a message to the console so that we can start experimenting with them. If you want to play with them yourself, feel free to copy the code below into a file and open it in the browser of your choice.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8"/>
    <title>Exit Intent Demo</title>
    <script>
      function mouseleaveCallback(event) {
        console.log('mouseleave');
      }

      function mouseoutCallback(event) {
        console.log('mouseout');
      }

      document.addEventListener('mouseleave', mouseleaveCallback);
      document.addEventListener('mouseout', mouseoutCallback);
    </script>
  </head>
  <body>
    <h1>Exit Intent Demo</h1>
  </body>
</html>

Both callbacks receive an object that implements the MouseEvent interface as a parameter, a specialization of the Event interface. One of the properties of the Event interface is target, which is a reference to the element to which the event was dispatched (i.e., the recipient element of the event). In the case of mouseout and mouseleave, the target is a reference to the element that the mouse pointer has left.

Additionally, and more relevant to us, the MouseEvent interface provides a property called relatedTarget, which is a reference to the element that the mouse pointer has entered. Note that this property is optional, meaning it can be null, which is the case if the mouse pointer has left the viewport. Needless to say, this is precisely what we are looking for.

function mouseoutCallback(event) {
  if (event.relatedTarget === null) {
    console.log('exit intent => open popup');
  }
}

function mouseleaveCallback(event) {
  if (event.relatedTarget === null) {
    console.log('exit intent => open popup');
  }
}

document.addEventListener('mouseleave', mouseoutCallback);
document.addEventListener('mouseleave', mouseleaveCallback);

This code detects exit intent whenever the visitor's mouse pointer leaves the viewport, but this is probably too much and would open the popup too often. Instead, we can take the direction of the mouse pointer into account so that we can detect exit intent only if the pointer leaves the viewport in an upward direction.

The reason for this is that the buttons to close tabs and windows are usually located above the viewport. Thus, we can probably ignore it if the mouse pointer leaves the viewport in a different direction. Taking the direction into account will cause us to miss the exit intent of some visitors, but this is fine. Exit intent is a heuristic, and it was already imperfect before we took direction into account. For instance, if visitors close a tab with their keyboard, the mouse events we rely on will never be triggered.

In any case, the MouseEvent interface has another property called clientY that we can use. This is the Y position of the mouse pointer in viewport coordinates (i.e., it's not affected by scroll position). With that in mind, we can adjust our event listener only to detect exit intent if the mouse pointer is close to the upper boundary of the viewport, suggesting that the mouse pointer will leave the viewport in an upward direction.

function mouseoutCallback(event) {
  if (event.relatedTarget === null && event.clientY <= 10) {
    console.log('exit intent => open popup');
  }
}

function mouseleaveCallback(event) {
  if (event.relatedTarget === null && event.clientY <= 10) {
    console.log('exit intent => open popup');
  }
}

document.addEventListener('mouseleave', mouseoutCallback);
document.addEventListener('mouseleave', mouseleaveCallback);

That's already a pretty good implementation, but there's one more improvement to make it less intrusive to visitors. That is, we shouldn't automatically open the popup again after a visitor has manually closed it. We can use a boolean variable to ensure that we open the popup only the first time we detect exit intent.

let exitIntent = false;

function mouseoutCallback(event) {
  if (!exitIntent && event.relatedTarget === null && event.clientY <= 10) {
    exitIntent = true;
    console.log('exit intent => open popup');
  }
}

function mouseleaveCallback(event) {
  if (!exitIntent && event.relatedTarget === null && event.clientY <= 10) {
    exitIntent = true;
    console.log('exit intent => open popup');
  }
}

document.addEventListener('mouseleave', mouseoutCallback);
document.addEventListener('mouseleave', mouseleaveCallback);

We don't need two event listeners, but the question is which one to keep. Both work, for sure, but they are redundant. To make an educated decision about which to keep, you must first understand the difference between them.

What's the difference between mouseout and mouseleave?

The mouseout and mouseleave are similar but have slightly different semantics.

Contrary to mouseout events, mouseleave events don't bubble up to parent elements in the DOM. So, event listeners such as someElement.addEventListener( 'mouseout', function(e) {} ) get called much more often because they receive events from descendent elements that have bubbled up. Every time the mouse pointer leaves one descendent element and enters another, the event handler is called.

On the other hand, the event listener someElement.addEventListener( 'mouseleave', function(e) {} ) is only called when the mouse pointer leaves someElement. If the pointer moves from one descendant to another, it doesn't call the event handler.

mouseleave is better for Exit Intent

I recommend mouseleave for exit intent because we don't care about the mouse pointer moving between descendent elements. You would need to filter out the events that bubbled up anyway because they're, by definition, not indicative of exit intent.

It makes no sense to listen for all of these events if you ignore most of them anyway. Again, both listeners work, but from a performance point of view, it's better to have event listeners that are called less often.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <script>
      let exitIntent = false;

      function trackExitIntent(event) {
        if (!exitIntent && event.relatedTarget === null && event.clientY <= 10) {
          exitIntent = true;
          console.log('exit intent => open popup');
        }
      }

      document.addEventListener('mouseleave', trackExitIntent);
    </script>
    <title>Exit Intent Demo</title>
  </head>
  <body>
    <h1>Exit Intent Demo</h1>
  </body>
</html>

There you have it. This is a solid trigger for opening popups to prompt visitors to subscribe to your mailing list before leaving your website. Thank you very much for reading, and see you soon! Also, don't forget to subscribe to my mailing list if you found this article interesting and have learned something new.

Christoph Schiessl

Hi, I'm Christoph Schiessl.

I help you build robust and fast Web Applications.


I'm available for hire as a freelance web developer, so you can take advantage of my more than a decade of experience working on many projects across several industries. Most of my clients are building web-based SaaS applications in a B2B context and depend on my expertise in various capacities.

More often than not, my involvement includes hands-on development work using technologies like Python, JavaScript, and PostgreSQL. Furthermore, if you already have an established team, I can support you as a technical product manager with a passion for simplifying complex processes. Lastly, I'm an avid writer and educator who takes pride in breaking technical concepts down into the simplest possible terms.

Continue Reading?

Here are a few more Articles for you ...


Tracking Scroll Depth to Measure Visitor Engagement

Calculate Scroll Depth as a percentage of page height and send custom events to Plausible, the GDPR-compliant analytics platform. Vanilla JavaScript only.

By Christoph Schiessl on JavaScript

The Built-In any() Function

Learn how to use the built-in any() function in Python to determine if any element in an iterable is True, with implementation and performance insights.

By Christoph Schiessl on Python

The Built-In id() Function

Learn about object identities and comparisons in Python. Discover the built-in id() function, the is and is not operators, and more.

By Christoph Schiessl on Python

Web App Reverse Checklist

Ready to Build Your Next Web App?

Get my Web App Reverse Checklist first ...


Software Engineering is often driven by fashion, but swimming with the current is rarely the best choice. In addition to knowing what to do, it's equally important to know what not to do. And this is precisely what my free Web App Reverse Checklist will help you with.

Subscribe below to get your free copy of my Reverse Checklist delivered to your inbox. Afterward, you can expect one weekly email on building resilient Web Applications using Python, JavaScript, and PostgreSQL.

By the way, it goes without saying that I'm not sharing your email address with anyone, and you're free to unsubscribe at any time. No spam. No commitments. No questions asked.