The "Off" Click
Tags: javascript, coding, design, webdev
Published: 2021-01-20 10:11:38

> Clicking anywhere off an element in Javascript ✋

I don’t know the technical term for this scenario 😅 We’ve likely all encountered it though. That scenario where you’ve bound a click to open or activate something. But, you also want a click bound to clicking anywhere else that closes it.

What is the technical term for that? I’m going to call it the "off" click.

Consider this common example using a side menu. You click the menu button to open the menu. When you go to click off it, you expect it to close. It shouldn’t be the case that it only closes when you click the menu button again.

For those in camp **TL;DR** , the solution is bind an "Off" click method to the `document` within your initial click handler. This gets added and removed when necessary. Here’s a demo!

{% codepen https://codepen.io/jh3y/pen/jZJNOG %}

Before we go any further. This isn’t just applicable to the side menu scenario. It could be used in other scenarios you may come across. It is also true, that we could use other methods to close the sliding menu such as a clickable overlay that fills the rest of the page.

## A Side Menu

For our example, we are going to use a basic side menu that slides in and out. The menu contains items that allow the user to change the background color of the page.

## Basic opening and closing

To open and close the side nav, we will apply a class to the menu element. The class will dictate the transform of the menu element sliding it right and left.

So what might the code look like for this? In the most basic implementation we can toggle the class on the menu element.

```javascript
const OPEN_CLASS = 'menu--open'
const menu = document.querySelector('.menu')
const menuBtn = menu.querySelector('.menu__button')
// Most basic method
menuBtn.addEventListener('click', () => menu.classList.toggle(OPEN_CLASS))
```

But, this isn’t ideal. We can open the menu, but the only way to close it is by clicking the menu button again.

![Showing how "off" click does nothing](https://cdn-images-1.medium.com/max/800/1*87u9lXJqAMBGYNzwKyqP6w.gif)

That won’t do 👎

## Introducing the "Off" click

How do we deal with this? We need an "Off" click.

Instead of only toggling the class, we can also bind an event listener to the document at the same time. The function we bind to can then close the menu. This means clicking anywhere on the document will close the menu.

```javascript
const offClick = () => {
  menu.classList.toggle(OPEN_CLASS)
  document.removeEventListener('click', offClick)
}
const handleClick = e => {
  menu.classList.toggle(OPEN_CLASS)
  if (menu.classList.contains(OPEN_CLASS)) {
    document.addEventListener('click', offClick)
  }
}
menuBtn.addEventListener('click', handleClick)
```

Wait, that doesn’t work…

![Menu not opening](https://cdn-images-1.medium.com/max/800/1*DC1zZAU5kOQpMdtimtqhMQ.gif)

The reason? Event propagation. We bind the document click in the same instance as clicking to open the menu, the event propagates. That means the menu opens and closes so quick we never see it. Let’s fix that!

```javascript
const offClick = () => {
  menu.classList.toggle(OPEN_CLASS)
  document.removeEventListener('click', offClick)
}
const handleClick = e => {
  e.stopPropagation()
  menu.classList.toggle(OPEN_CLASS)
  if (menu.classList.contains(OPEN_CLASS)) {
    document.addEventListener('click', offClick)
  }
}
menuBtn.addEventListener('click', handleClick)
```

To fix it we could use `stopPropagation`. This will stop the event bubbling that makes the menu close when it should be open.

![Menu opening and closing](https://cdn-images-1.medium.com/max/800/1*5Ae7c9KJvEfctQlxRwgZOQ.gif)

But the use of `stopPropagation` could introduce other pitfalls that we want to avoid 👻

## Be wary of using stopPropagation

The use of `stopPropagation` can be risky and can introduce bugs if not used with care 🐛 In this solution, we are only using it on the click handler of the menu button. We are stopping the click event from bubbling up.

But using `stopPropagation` means that we create a clicking deadzone. How? Later down the line, we may decide to add an event handler to a parent of the menu button. But if we click the menu button, the event wont propagate. That means the new event handler wouldn’t fire 😢

![Menu Button creating a deadzone click. Clicking the body increases a number on the page. But, clicking the button does nothing.](https://cdn-images-1.medium.com/max/800/1*LgKwZcZGSqwKke_X1vbv4g.gif)

## Comparing events

One solution is to compare the event objects. We can encapsulate the logic within our menu button click handler.

```javascript
const handleClick = e => {
  const offClick = evt => {
    if (e !== evt) {
      menu.classList.toggle(OPEN_CLASS)
      document.removeEventListener('click', offClick)
    }
  }
  if (!menu.classList.contains(OPEN_CLASS)) {
    menu.classList.toggle(OPEN_CLASS)
    document.addEventListener('click', offClick)
  }
}
menuBtn.addEventListener('click', handleClick)
```

This way, we aren’t stopping the event propagation.

![No more deadzone. Clicking the button triggers both actions. It increases the number but also opens and closes the menu.](https://cdn-images-1.medium.com/max/800/1*qGKxc-FaX9W0SOh6gweL1Q.gif)

This way we can still propagate the event but make sure that we don’t fall into the instant open and close issue.

## Can we extract that logic?

Yes. It’s unlikely you’ll have to cater for the "Off" click in several places across your app, but it won’t hurt to refactor.

```javascript
const addOffClick = (e, cb) => {
  const offClick = evt => {
    if (e !== evt) {
      cb()
      document.removeEventListener('click', offClick)
    }
  }
  document.addEventListener('click', offClick)
}
```

We could now apply the behavior across different parts of our app 🎉

```javascript
const handleClick = e => {
  const toggleMenu = () => menu.classList.toggle(OPEN_CLASS)
  if (!menu.classList.contains(OPEN_CLASS)) {
    toggleMenu()
    addOffClick(e, toggleMenu)
  }
}
```

## That’s it!

A quick look at the "Off" click scenario. We’ve covered how to handle it whilst avoiding the use of `stopPropagation`.

{% codepen https://codepen.io/jh3y/pen/jZJNOG %}

---

Any good? [Let me know](https://twitter.com/jh3yy)! Let's connect!