Implement an exit intent modal in React

Last updated on December 20, 2021

Many websites (including this one) have implemented exit intent modals. I have taken a lighter approach and have implemented a popup banner/modal, and I will show you how I did it. You can easily extend this example to be a modal instead if you prefer this approach.

What is exit intent anyways?

Exit intent strategy is displaying a modal before the user leaves the page. You can detect when the mouse moves to the tab portion of the browser (+ a certain threshold which we will use in this example) and display the modal then. We will then set up a cookie with an expiration date so we don’t annoy people with the modal every time they want to exit our articles.

Try to trigger the exit intent banner on this website to see how it works.

Extract exit intent logic into a component

We will use this npm package but as it is only one file, we can just import it into our utils folder and use it within our app. That way we can decrease the dependencies of our app and have the ability to extend the component logic if we need to.

// src/utils/ExitIntent.js

import throttle from "lodash/throttle";

export default function ExitIntent(options = {}) {
  const defaultOptions = {
    threshold: 20,
    maxDisplays: 1,
    eventThrottle: 200,
    onExitIntent: () => {},
  };

  return (function () {
    const config = { ...defaultOptions, ...options };
    const eventListeners = new Map();
    let displays = 0;

    const addEvent = (eventName, callback) => {
      document.addEventListener(eventName, callback, false);
      eventListeners.set(`document:${eventName}`, { eventName, callback });
    };

    const removeEvent = (key) => {
      const { eventName, callback } = eventListeners.get(key);
      document.removeEventListener(eventName, callback);
      eventListeners.delete(key);
    };

    const shouldDisplay = (position) => {
      if (position <= config.threshold && displays < config.maxDisplays) {
        displays++;
        return true;
      }
      return false;
    };

    const mouseDidMove = (event) => {
      if (shouldDisplay(event.clientY)) {
        config.onExitIntent();
        if (displays >= config.maxDisplays) {
          removeEvents();
        }
      }
    };

    const removeEvents = () => {
      eventListeners.forEach((value, key, map) => removeEvent(key));
    };

    addEvent("mousemove", throttle(mouseDidMove, config.eventThrottle));

    return removeEvents;
  })();
}

Let’s take a moment to appreciate what this code is doing

Set up a useEffect hook

Our next step is to create a useEffect hook within the component where we want to trigger the exit intent. Whenever the page is loaded, we initialize the ExitIntent.

Since ExitIntent returns the function that removes the exit intent events, we assign it as a constant which we then give as the return value of our useEffect.

If you need a refresher of the basics of useEffect, you can read a bit about them in theofficial react docs.

// article.js

import React, { useState, useEffect } from "react"
import ExitIntent from "../utils/ExitIntent"

...

const Article = props => {
  const [showPopup, setShowPopup] = useState(false)

  useEffect(() => {
    const removeExitIntent = ExitIntent({
      threshold: 30,
      eventThrottle: 100,
      onExitIntent: () => {
        setShowPopup(true)
      },
    })
    return () => {
      removeExitIntent()
    }
  })

...

  return(
    ...
    <ExitIntentModal show={showPopup} />
    ...
  )

The ExitIntentModal component

Now for the fun part. You can style this modal however you like - I will leave this part to you. However, you should be aware that you need set up a useEffect hook in this component as well in order to update your **show **variable whenever your parent component triggers the ExitIntent function (and therefore passes an updated show prop).

const ExitIntentModal = props => {
  // use show to determine if you should display the modal or not
  const [show, setShow] = useState(props.show)
  useEffect(() => {
    setShow(props.show)
  }, [props.show])

  // you can create a function to close the modal after the user clicks the "x" button or subscribes
  const close = () => {
    setShow(false)
  }

You probably don’t want to show the popup every time a user visits your articles (and then leaves). Imagine if a user goes through 5 of your articles, it would be bad UI if the user happens to see the popup 5 times (even if they are only triggered at exit events).

Let’s create a cookie when the user sees the modal, but set up an expiry date of 14 days.

If props.show is true, we create a new **modal_seen **cookie, which expires 14 days from now.

// ExitIntentModal.js

const ExitIntentModal = props => {
  const [show, setShow] = useState(props.show)
  useEffect(() => {
    if (props.show) {
      let expiryDate = new Date(
        Date.now() + 14 * (1000 * 60 * 60 * 24)
      )
      expiryDate.setFullYear(expiryDate.getFullYear() + 1)
      document.cookie =
        "modal_seen" + "=true; expires=" + expiryDate.toUTCString()
    }
    setShow(props.show)
  }, [props.show])

// ...

Whenever you create the cookie, don’t forget to use.toUTCString()to convert the date to a string. Note that we don’t really care about timezones, because they won’t have a significant impact.

We finally need to modify our useEffect within our article.js component so it only renders the ExitIntent logic if we don’t have the modal_seen cookie yet.

// article.js

...

  useEffect(() => {
    if (document.cookie.indexOf("modal_seen=true") < 0) {
      const removeExitIntent = ExitIntent({
        threshold: 30,
        eventThrottle: 100,
        onExitIntent: () => {
          setShowPopup(true)
        },
      })
      return () => {
        removeExitIntent()
      }
    }
  })

...

**document.cookie.indexOf(“modal_seen=true”) < 0 **checks if this particular cookie is present in the browser. If it’s not, set up the logic for ExitIntent. If not, don’t do anything.

That’s about it - you can now focus on creating the modal and converting your customers just before they leave the page.

Invite us to your inbox.

Articles, guides and interviews about web development and career progression.

Max 1-2x times per month.