🛠 Modal: Closing the modal

Hey ,

I'm thrilled to help you learn JavaScript. Unfortunately, you've landed on a page where you cannot access with your current purchase.

Please upgrade (use this link) access this content.

I'm super eager to help you learn more!

🛠 Modal: Closing the modal

You’ll learn how to close the modal when a user clicks outside of the modal.

Close modal by clicking outside the modal.

Closing the modal when a user clicks outside

If a user clicks outside the modal, they’re actually clicking on the modal overlay.

Anatomy of a modal.

If you want to detect whether a user clicked on the overlay, the easiest way is to add an event listener on the overlay itself.

const modalOverlay = document.querySelector('.modal-overlay')

modalOverlay.addEventListener('click', event => {
  // Do something here
})

To close the modal, we remove .modal-is-open from <body>.

modalOverlay.addEventListener('click', event => {
  document.body.classList.remove('modal-is-open')
})

This seems to work fine. The modal closes when you click on the overlay. But the modal also closes when you click inside the modal!

Modal closes when you click outside. But modal also closes when you click inside the modal!

This should not happen. People click around when they read. If they click on content inside the modal, they’re still interested in the content. You want to keep the modal open.

Preventing clicks within the modal from closing the modal

When you click on the modal, the click event bubbles upwards towards the overlay. When this click event hits the overlay, it triggers the event listener we just wrote. And it closes the modal.

This is why the modal closed when you clicked inside it. You can tell that this is happening if you console.log the event target. In this case, you can see I clicked on the hand inside the modal.

modalOverlay.addEventListener('click', event => {
  document.body.classList.remove('modal-is-open')
  console.log(event.target)
})
Logging the event target.

The most straightforward way to prevent the modal from closing is to prevent the event from bubbling upwards.

const modal = document.querySelector('.modal')

modal.addEventListener('click', event => {
  event.stopPropagation()
})
Prevented the modal from closing.

This works, but there’s a tiny problem with stopPropagation.

The problem with stopPropagation

The problem with stopPropagation is it prevents all events of that type from passing through. Let’s go through an example to put this scenario into perspective.

Say you have the following HTML, and you’ve prevented click events from bubbling from the modal through stopPropagation.

<body>
  <div class="modal-container">
    <div class="modal"></div>
  </div>
</body>
const modal = document.querySelector('.modal')
modal.addEventListener('click', event => {
  event.stopPropagation()
})

What if you need to listen for a click event on <body>, and you expect this click event to bubble upwards from inside the modal?

This click event on <body> will not trigger because you stopped the event from bubbling upwards. The event will never reach <body>.

That’s the problem with stopPropagation.

A better way

Let’s look at the anatomy of a modal again. You’ll notice the user can only click three things:

  1. Inside the modal.
  2. The modal itself.
  3. The overlay.
Anatomy of the modal.

Every event will bubble upwards and reach the overlay. But events inside the modal and events on the modal will reach the modal first, before going to the overlay.

Event sequence:

  • Click inside modal: The element -> modal -> modal overlay
  • Click modal: modal -> modal overlay
  • Click overlay: modal overlay

This means we can check whether the event passes through the modal. If it passes through the modal, we know we should not close the modal.

In JavaScript, we can’t check whether an event passes through an element directly, but we have a good proxy for this. We can check if the modal is an ancestor of the event.target. If the modal is an ancestor, we know the event will bubble through the modal.

We can use closest to check whether the modal is an ancestor of the event.target. closest also checks if the event.target matches the given selector.

modalOverlay.addEventListener('click', event => {
  if (event.target.closest('.modal')) {
    // Do nothing
  } else {
    // Close modal
    document.body.classList.remove('modal-is-open')
  }
})

We can clean up the code a bit by using the NOT operator.

modalOverlay.addEventListener('click', event => {
  if (!event.target.closest('.modal')) {
    // Close modal
    document.body.classList.remove('modal-is-open')
  }
})
Prevented the modal from closing.

That’s it!

Exercise

Close your modal when a user clicks on outside the modal. Make sure you don’t use stopPropagation.