Implement an exit intent modal in React

Published in May 2020

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

  • We define several default options, which we can configure when we call the object later
  • A note that throttle doesn't allow a function to execute more than once in a given period. Therefore, by default, we only allow the function to execute once every 200ms (it makes things more efficient and doesn't put a burden on the browser)
  • We add a mousemove event listener using the addEvent function. We check if we should execute our onExitIntent prop function or not.
  • We check the Y position of the cursor and if it's above the top of the viewport of the page (+ any threshold we have set), we return true.
  • This component also gives us the ability to set up how many times we want it to trigger per page load, but for our purposes, we only one it to happen once (as it is the default)
  • Finally, the return value of this function is removeEvents, which unsurprisingly, clears all events.

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 the official 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)
  }
...

Add a cookie with an expiry date

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.

Thanks for reading 👋

Level up your web development skills

Get articles, guides and interviews right in your inbox. Join a community of fellow developers.

No spam. Unsubscribe at any time.

Full Stack Heroes logo

© 2020 Full Stack Heroes