How to close popup when clicking outside it using JavaScript

Mads AkselsenMads Akselsen
6 min read

Why not use backdrops

Backdrops are a great way of bringing attention to a popup. Furthermore, it indicates the need for user interaction before the user is allowed to interact with other parts of the page.

Backdrops are also an easy way of coding the functionality for closing a popup.

All the developer needs to do is add a div behind the popup that takes up the whole screen, and then add a click event to it.

While backdrops are an easy way of removing a popup when clicking outside it, it comes with caveats:

  • Developers have to add a backdrop behind every popup.

  • Backdrops 'absorb' a click, so the user must click twice to interact with the elements behind it, making an awkward user experience. This is why backdrops are most often made visible to the user, to indicate what will happen if they click on them.

Use mouse position and element bounding box

Backdrops can be avoided with the use of the mouse position and the popup bounding box. In essence, the popup will close if the mouse click occurs outside the popup bounding box.

In the following, I will take you through how to create a class to handle closing any popups in your application. Basically, the class will contain the state and all the logic for handling closing the popups, and there will be just a single public function that can be applied wherever it's needed.

Here's the result of what we will make:

Here's the repo if you would like to clone it.

Create the project files

We need to create four files for this project:

  • index.html

  • styles.css

  • script.js

  • close-popup-manager.js

Let’s start by creating the buttons along with three popups:

index.html

<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <meta http-equiv="X-UA-Compatible" content="IE=edge">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Document</title>

   <link rel="stylesheet" type="text/css" href="styles.css" media="screen" />

</head>
<body>
   <button id="button-1">Button 1</button>
   <button id="button-2">Button 2</button>
   <button id="button-3">Button 3</button>

   <div class="popup" id="popup-1">
       <span>Popup 1</span>
   </div>
   <div class="popup" id="popup-2">
       <span>Popup 2</span>
   </div>
   <div class="popup" id="popup-3">
       <span>Popup 3</span>
   </div>

   <script src='./close-popup-manager.js'></script>
   <script src='./script.js'></script>
</body>
</html>

styles.css

button {
   width: 100px;
   height: 40px;
   font-size: 14px;
   font-weight: bold;
   background: #FFFFFF;
   cursor: pointer;
}

button:focus {
   background-color: #777777;
   color: #FFFFFF;
}

.popup {
   position: absolute;
   top: 60px;
   transform: translateX(-105%);
   transition: transform 150ms ease-out;
   box-shadow: 2px 2px 8px grey;
   border-radius: 4px 4px 4px 4px;
   color: #FFFFFF;
   font-size: 20px;
   font-weight: bold;
}

.popup[shown] {
   display: block;
   transform: translateX(0%);
   transition: transform 150ms ease-in;
   display: flex;
   justify-content: center;
   align-items: center;
}

#popup-1 {
   background-color: blue;
   width: 400px;
   height: 700px;
}

#popup-2 {
   background-color: red;
   width: 350px;
   height: 250px;
}

#popup-3 {
   background-color: green;
   width: 200px;
   height: 500px;
}

The important part to notice here is the .popup[shown] selector. It will apply styles (making the popup visible) when the element contains the shown attribute. We will toggle this attribute through JavaScript in the next section.

Create class to manage closing popups

To make life easy for developers, we will create a class that contains the logic for closing popups. The result will be a single function that can be applied to every element that should be closed when clicking outside it.

The following code will be written in close-popup-manager.js .

1. Create the class

The first step is to create the class. Notice it is immediately initialized with new class. This ensures that only one instance of the class exists and you don’t have to initialize it anywhere. In other words, we now have a single source of truth.

const closePopupManager = new class {

}

2. Create a function for adding elements to the class

The class need to know about the elements it needs to close, so we will add those now.

   //  element: Element|HTMLElement
   //  ctx: any
   //  closeFn: Function
   async addElement(closeFn, ctx, element) {
       // wrapping in setTimeout to make sure the element has been         added to the DOM.
       // Otherwise, getBoundingClientRect() don't know the coordinates of the element.
       setTimeout(() => {
           this._elements.push({
               element,
               ctx,
               closeFn
           });

       }, 100)
   }

   _elements = [];

The function receives three arguments, closeFn, ctx and element.

  • The element is the popup that should be closed when the user at some point clicks outside it.

  • closeFn is the function that handles closing the popup. It will be created in script.js later. We could also have made a generic function to handle the 'close' logic, but by passing in a custom function the developer gets more freedom to make different functionality when needed.

  • ctx is the context in which the function should be run. This ensures that if the user provides a normal function, i.e. not an arrow function, the function still works. The reason is that normal functions (function() {}) do not bring along the context in which they were created. So when the class runs the function, it tries to run without the context, and you will get an error about the function being undefined. This is not the case with an arrow function (const myFunction = () => {}), which brings along its context.

    So passing the ctx here just makes sure that the function works regardless of the user passing a normal or arrow function.

The arguments are stored in _elements array. The underscore indicates that it is a private property because it is internal logic in the class, that developers should not mess with when using the class.

The setTimeout makes sure that the element has been added to the DOM before it is pushed to the array. This makes sure that the browser had time to create the whole render tree, so it knows the size and coordinates of all elements, which is required to get the bounding box of the popup.

3. Create function closing popups

The next step is to create a function that will iterate through all the elements in _elements on every user click, and check whether or not the click is outside the bounding box of each element. If the click is detected to be outside an element, then the corresponding closeFn will be run.

_closeElements(e) {
       const x = e.clientX;
       const y = e.clientY;

       for (const [i, el] of this._elements.entries()) {
           const rect = el.element.getBoundingClientRect();
           const {top, right, bottom, left} = rect;

           if (x < left || x > right || y < top || y > bottom) {
               el.closeFn.apply(el.ctx);
               this._elements.splice(i, 1);
           }
       }
   }

3. Make the function run on click

The logic for running _closeElements on clicks will be added in the constructor:

   constructor() {
       window.addEventListener("click", (e) => {
           this._closeElements(e);
       })
   }

Put the close-popup-manager to use

Now it’s time to see the class in action, so let’s create the code that will interact with the buttons and popups.

The following code will be written in script.js.

1. Assign the buttons and popups to variables

const button1 = document.querySelector("#button-1");
const button2 = document.querySelector("#button-2");
const button3 = document.querySelector("#button-3");

const popup1 = document.querySelector("#popup-1");
const popup2 = document.querySelector("#popup-2");
const popup3 = document.querySelector("#popup-3");

button1.addEventListener("click", () => togglePopup("popup-1"));
button2.addEventListener("click", () => togglePopup("popup-2"));
button3.addEventListener("click", () => togglePopup("popup-3"));

togglePopup function will be created in the next step.

2. Create function for toggling popups

function togglePopup(popupName) {
   const element = document.getElementById(popupName);
   const popupShown = element.getAttribute("shown");

   if (popupShown) {
       element.removeAttribute("shown");
   } else {
       element.setAttribute("shown", true);

       const closePopup = () => {
           element.removeAttribute("shown");
       }
       elementCloseManager.addElement(closePopup, this, element);
   }
}

Voila! Now all the popups will close when clicking outside of their bounding box.

Feel free to reach out if you got any questions or feedback :-)

0
Subscribe to my newsletter

Read articles from Mads Akselsen directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Mads Akselsen
Mads Akselsen

I am a developer currently working as a Software Engineer at A.P. Moller - Maersk