Implementing Clicking Outside of a Menu in JavaScript

nextjs
reactjs
javascript
web
dev
tutorial

Published at: 03/07/2025

A common feature in web development is closing a popup or menu when the user clicks outside of it. While the implementation is straightforward, there are a few nuances to understand, especially when using different tools and libraries. In this tutorial, I will walk you through examples to illustrate the ideas.

React/Next.js Implementation

In React or Next.js, you can easily implement the “clicking outside to close” feature using the useEffect hook. Here’s an example implementation:

const [isMenuVisible, setIsMenuVisible] = React.useState(false)

React.useEffect(() => {
  const handleClickingOutside = (event) => {
    if (menuRef.current && !menuRef.current.contains(event.target)) {
      setIsMenuVisible(false)
    }
  }
  if (isMenuVisible) {
    document.addEventListener("mousedown", handleClickingOutside)
  }
  return () => document.removeEventListener("mousedown", handleClickingOutside)
}, [isMenuVisible])

Why mousedown

In the above example, we listen for the mousedown event rather than click. This choice is key to ensuring the outside-click handler works as intended. Here with React/Next, we can safely listen to click instead:

React.useEffect(() => {
  const handleClickingOutside = (event) => {
    if (menuRef.current && !menuRef.current.contains(event.target)) {
      setIsMenuVisible(false)
    }
  }
  if (isMenuVisible) {
    document.addEventListener("click", handleClickingOutside) // Listening to click
  }
  return () => document.removeEventListener("click", handleClickingOutside) // Listening to click
}, [isMenuVisible])

It seems React/Next.js can handle event propagation and state changes in a way that avoids issues. However, with plain Javascript, we need to choose mousedown.

Plain JavaScript Implementation

In vanilla Javacript, when you listen to a click event to open the menu and another to close it, the menu closes as soon as it opens, leading to an impression that the menu does not open at all.

Here is an example:

<style>
  #menu {
    background: green;
    display: none;
  }
  #menu.open {
    display: block;
  }
</style>
<button id="open">open</button>
<ul id="menu">
  <li>foo</li>
  <li>bar</li>
</ul>

The followig code does not work:

const btn = document.getElementById("open")
const menu = document.getElementById("menu")

function handleOpen() {
  console.log("handleOpen")
  if (!menu.classList.contains("open")) {
    menu.classList.add("open")
  }
}

function handleClickOutside(e) {
  console.log("handleClickOutside")
  if (menu.classList.contains("open") && !menu.contains(e.target)) {
    menu.classList.remove("open")
  }
}

btn.addEventListener("click", handleOpen)
document.addEventListener("click", handleClickOutside) // Listening to click does not work

The Problem with click

When using the click event, the menu immediately closes after opening because both the click to open the menu and the click to detect if the user is outside the menu are triggered in quick succession.

Menu immediately closes after opening
Menu immediately closes after opening

We can see problem more clearly by adding a small delay to the removal of the open class name:

function handleClickOutside(e) {
  console.log("handleClickOutside")
  if (menu.classList.contains("open") && !menu.contains(e.target)) {
    setTimeout(() => {
      menu.classList.remove("open")
    }, 2000)
  }
}

A small delay helps highlight the issue
A small delay helps highlight the issue

The menu closes automatically after two seconds.

The Solution: Use mousedown

To fix this issue, we switch to listening for the mousedown event, which fires at a different time than the click event. This ensures that the menu opens fully before we check if the user clicked outside.

Here’s the updated code that works:

document.addEventListener("mousedown", handleClickOutside)

Additionally, listening to mousedown provides a quicker response time. On mobile devices, this leads to smoother interaction, as the menu closes immediately when the user touches outside of it.

Conclusion

In summary:

  • In React/Next.js, both mousedown and click work, thanks to their reconciliation mechanism.
  • In plain JavaScript, the issue arises when using the click event because it triggers after the menu is opened, leading to unintended behavior.
  • Using mousedown resolves this issue and also provides faster and smoother interaction.

By understanding the underlying event flow, you can ensure that the “clicking outside to close” feature works as expected in both React/Next.js and plain JavaScript.


You May Also Like