Categories
click javascript jquery

How do I detect a click outside an element?

2812

I have some HTML menus, which I show completely when a user clicks on the head of these menus. I would like to hide these elements when the user clicks outside the menus’ area.

Is something like this possible with jQuery?

$("#menuscontainer").clickOutsideThisElement(function() {
    // Hide the menus
});

7

1967

Note: Using stopPropagation is something that should be avoided as it breaks normal event flow in the DOM. See this CSS Tricks article for more information. Consider using this method instead.

Attach a click event to the document body which closes the window. Attach a separate click event to the container which stops propagation to the document body.

$(window).click(function() {
  //Hide the menus if visible
});

$('#menucontainer').click(function(event){
  event.stopPropagation();
});

10

  • 795

    This breaks standard behaviour of many things, including buttons and links, contained within #menucontainer. I am surprised this answer is so popular.

    – Art

    Jun 12, 2010 at 8:00

  • 81

    This doesn’t break behavior of anything inside #menucontainer, since it is at the bottom of the propagation chain for anything inside of it.

    Jun 16, 2010 at 19:55

  • 101

    its very beautyfull but you should use $('html').click() not body. The body always has the height of its content. It there is not a lot of content or the screen is very high, it only works on the part filled by the body.

    – meo

    Feb 25, 2011 at 15:35

  • 113

    I am also surprised that this solution got so many votes. This will fail for any element outside that has stopPropagation jsfiddle.net/Flandre/vaNFw/3

    – Andre

    May 8, 2012 at 12:32

  • 144

    Philip Walton explains very well why this answer isn’t the best solution: css-tricks.com/dangers-stopping-event-propagation

    – Tom

    May 20, 2014 at 10:52

1565

You can listen for a click event on document and then make sure #menucontainer is not an ancestor or the target of the clicked element by using .closest().

If it is not, then the clicked element is outside of the #menucontainer and you can safely hide it.

$(document).click(function(event) { 
  var $target = $(event.target);
  if(!$target.closest('#menucontainer').length && 
  $('#menucontainer').is(":visible")) {
    $('#menucontainer').hide();
  }        
});

Edit – 2017-06-23

You can also clean up after the event listener if you plan to dismiss the menu and want to stop listening for events. This function will clean up only the newly created listener, preserving any other click listeners on document. With ES2015 syntax:

export function hideOnClickOutside(selector) {
  const outsideClickListener = (event) => {
    const $target = $(event.target);
    if (!$target.closest(selector).length && $(selector).is(':visible')) {
        $(selector).hide();
        removeClickListener();
    }
  }

  const removeClickListener = () => {
    document.removeEventListener('click', outsideClickListener);
  }

  document.addEventListener('click', outsideClickListener);
}

Edit – 2018-03-11

For those who don’t want to use jQuery. Here’s the above code in plain vanillaJS (ECMAScript6).

function hideOnClickOutside(element) {
    const outsideClickListener = event => {
        if (!element.contains(event.target) && isVisible(element)) { // or use: event.target.closest(selector) === null
          element.style.display = 'none';
          removeClickListener();
        }
    }

    const removeClickListener = () => {
        document.removeEventListener('click', outsideClickListener);
    }

    document.addEventListener('click', outsideClickListener);
}

const isVisible = elem => !!elem && !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length ); // source (2018-03-11): https://github.com/jquery/jquery/blob/master/src/css/hiddenVisibleSelectors.js 

NOTE:
This is based on Alex comment to just use !element.contains(event.target) instead of the jQuery part.

But element.closest() is now also available in all major browsers (the W3C version differs a bit from the jQuery one).
Polyfills can be found here: Element.closest()

Edit – 2020-05-21

In the case where you want the user to be able to click-and-drag inside the element, then release the mouse outside the element, without closing the element:

      ...
      let lastMouseDownX = 0;
      let lastMouseDownY = 0;
      let lastMouseDownWasOutside = false;

      const mouseDownListener = (event: MouseEvent) => {
        lastMouseDownX = event.offsetX;
        lastMouseDownY = event.offsetY;
        lastMouseDownWasOutside = !$(event.target).closest(element).length;
      }
      document.addEventListener('mousedown', mouseDownListener);

And in outsideClickListener:

const outsideClickListener = event => {
        const deltaX = event.offsetX - lastMouseDownX;
        const deltaY = event.offsetY - lastMouseDownY;
        const distSq = (deltaX * deltaX) + (deltaY * deltaY);
        const isDrag = distSq > 3;
        const isDragException = isDrag && !lastMouseDownWasOutside;

        if (!element.contains(event.target) && isVisible(element) && !isDragException) { // or use: event.target.closest(selector) === null
          element.style.display = 'none';
          removeClickListener();
          document.removeEventListener('mousedown', mouseDownListener); // Or add this line to removeClickListener()
        }
    }

6

  • 33

    I tried many of the other answers, but only this one worked. Thanks. The code I ended up using was this: $(document).click( function(event) { if( $(event.target).closest(‘.window’).length == 0 ) { $(‘.window’).fadeOut(‘fast’); } } );

    – Pistos

    Apr 5, 2012 at 19:30

  • 42

    I actually ended up going with this solution because it better supports multiple menus on the same page where clicking on a second menu while a first is open will leave the first open in the stopPropagation solution.

    Apr 7, 2012 at 23:41

  • 15

    Excellent answer. This is the way to go when you have multiple items which you wish to close.

    – John

    May 8, 2013 at 15:23

  • 26

    Without jQuery!element.contains(event.target) using Node.contains()

    – Alex Ross

    Jul 31, 2015 at 1:56

  • 1

    If you’re reading this then you should probably check out some of the more modern answers to solve this that are way more readable than this answer.

    – maxshuty

    Apr 24 at 14:58

397

How to detect a click outside an element?

The reason that this question is so popular and has so many answers is that it is deceptively complex. After almost eight years and dozens of answers, I am genuinely surprised to see how little care has been given to accessibility.

I would like to hide these elements when the user clicks outside the menus’ area.

This is a noble cause and is the actual issue. The title of the question—which is what most answers appear to attempt to address—contains an unfortunate red herring.

Hint: it’s the word “click”!

You don’t actually want to bind click handlers.

If you’re binding click handlers to close the dialog, you’ve already failed. The reason you’ve failed is that not everyone triggers click events. Users not using a mouse will be able to escape your dialog (and your pop-up menu is arguably a type of dialog) by pressing Tab, and they then won’t be able to read the content behind the dialog without subsequently triggering a click event.

So let’s rephrase the question.

How does one close a dialog when a user is finished with it?

This is the goal. Unfortunately, now we need to bind the userisfinishedwiththedialog event, and that binding isn’t so straightforward.

So how can we detect that a user has finished using a dialog?

focusout event

A good start is to determine if focus has left the dialog.

Hint: be careful with the blur event, blur doesn’t propagate if the event was bound to the bubbling phase!

jQuery’s focusout will do just fine. If you can’t use jQuery, then you can use blur during the capturing phase:

element.addEventListener('blur', ..., true);
//                       use capture: ^^^^

Also, for many dialogs you’ll need to allow the container to gain focus. Add tabindex="-1" to allow the dialog to receive focus dynamically without otherwise interrupting the tabbing flow.

$('a').on('click', function () {
  $(this.hash).toggleClass('active').focus();
});

$('div').on('focusout', function () {
  $(this).removeClass('active');
});
div {
  display: none;
}
.active {
  display: block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<a href="#example">Example</a>
<div id="example" tabindex="-1">
  Lorem ipsum <a href="http://example.com">dolor</a> sit amet.
</div>

If you play with that demo for more than a minute you should quickly start seeing issues.

The first is that the link in the dialog isn’t clickable. Attempting to click on it or tab to it will lead to the dialog closing before the interaction takes place. This is because focusing the inner element triggers a focusout event before triggering a focusin event again.

The fix is to queue the state change on the event loop. This can be done by using setImmediate(...), or setTimeout(..., 0) for browsers that don’t support setImmediate. Once queued it can be cancelled by a subsequent focusin:

$('.submenu').on({
  focusout: function (e) {
    $(this).data('submenuTimer', setTimeout(function () {
      $(this).removeClass('submenu--active');
    }.bind(this), 0));
  },
  focusin: function (e) {
    clearTimeout($(this).data('submenuTimer'));
  }
});

$('a').on('click', function () {
  $(this.hash).toggleClass('active').focus();
});

$('div').on({
  focusout: function () {
    $(this).data('timer', setTimeout(function () {
      $(this).removeClass('active');
    }.bind(this), 0));
  },
  focusin: function () {
    clearTimeout($(this).data('timer'));
  }
});
div {
  display: none;
}
.active {
  display: block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<a href="#example">Example</a>
<div id="example" tabindex="-1">
  Lorem ipsum <a href="http://example.com">dolor</a> sit amet.
</div>

The second issue is that the dialog won’t close when the link is pressed again. This is because the dialog loses focus, triggering the close behavior, after which the link click triggers the dialog to reopen.

Similar to the previous issue, the focus state needs to be managed. Given that the state change has already been queued, it’s just a matter of handling focus events on the dialog triggers:

This should look familiar

$('a').on({
  focusout: function () {
    $(this.hash).data('timer', setTimeout(function () {
      $(this.hash).removeClass('active');
    }.bind(this), 0));
  },
  focusin: function () {
    clearTimeout($(this.hash).data('timer'));  
  }
});

$('a').on('click', function () {
  $(this.hash).toggleClass('active').focus();
});

$('div').on({
  focusout: function () {
    $(this).data('timer', setTimeout(function () {
      $(this).removeClass('active');
    }.bind(this), 0));
  },
  focusin: function () {
    clearTimeout($(this).data('timer'));
  }
});

$('a').on({
  focusout: function () {
    $(this.hash).data('timer', setTimeout(function () {
      $(this.hash).removeClass('active');
    }.bind(this), 0));
  },
  focusin: function () {
    clearTimeout($(this.hash).data('timer'));  
  }
});
div {
  display: none;
}
.active {
  display: block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<a href="#example">Example</a>
<div id="example" tabindex="-1">
  Lorem ipsum <a href="http://example.com">dolor</a> sit amet.
</div>

Esc key

If you thought you were done by handling the focus states, there’s more you can do to simplify the user experience.

This is often a “nice to have” feature, but it’s common that when you have a modal or popup of any sort that the Esc key will close it out.

keydown: function (e) {
  if (e.which === 27) {
    $(this).removeClass('active');
    e.preventDefault();
  }
}

$('a').on('click', function () {
  $(this.hash).toggleClass('active').focus();
});

$('div').on({
  focusout: function () {
    $(this).data('timer', setTimeout(function () {
      $(this).removeClass('active');
    }.bind(this), 0));
  },
  focusin: function () {
    clearTimeout($(this).data('timer'));
  },
  keydown: function (e) {
    if (e.which === 27) {
      $(this).removeClass('active');
      e.preventDefault();
    }
  }
});

$('a').on({
  focusout: function () {
    $(this.hash).data('timer', setTimeout(function () {
      $(this.hash).removeClass('active');
    }.bind(this), 0));
  },
  focusin: function () {
    clearTimeout($(this.hash).data('timer'));  
  }
});
div {
  display: none;
}
.active {
  display: block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<a href="#example">Example</a>
<div id="example" tabindex="-1">
  Lorem ipsum <a href="http://example.com">dolor</a> sit amet.
</div>

If you know you have focusable elements within the dialog, you won’t need to focus the dialog directly. If you’re building a menu, you could focus the first menu item instead.

click: function (e) {
  $(this.hash)
    .toggleClass('submenu--active')
    .find('a:first')
    .focus();
  e.preventDefault();
}

$('.menu__link').on({
  click: function (e) {
    $(this.hash)
      .toggleClass('submenu--active')
      .find('a:first')
      .focus();
    e.preventDefault();
  },
  focusout: function () {
    $(this.hash).data('submenuTimer', setTimeout(function () {
      $(this.hash).removeClass('submenu--active');
    }.bind(this), 0));
  },
  focusin: function () {
    clearTimeout($(this.hash).data('submenuTimer'));  
  }
});

$('.submenu').on({
  focusout: function () {
    $(this).data('submenuTimer', setTimeout(function () {
      $(this).removeClass('submenu--active');
    }.bind(this), 0));
  },
  focusin: function () {
    clearTimeout($(this).data('submenuTimer'));
  },
  keydown: function (e) {
    if (e.which === 27) {
      $(this).removeClass('submenu--active');
      e.preventDefault();
    }
  }
});
.menu {
  list-style: none;
  margin: 0;
  padding: 0;
}
.menu:after {
  clear: both;
  content: '';
  display: table;
}
.menu__item {
  float: left;
  position: relative;
}

.menu__link {
  background-color: lightblue;
  color: black;
  display: block;
  padding: 0.5em 1em;
  text-decoration: none;
}
.menu__link:hover,
.menu__link:focus {
  background-color: black;
  color: lightblue;
}

.submenu {
  border: 1px solid black;
  display: none;
  left: 0;
  list-style: none;
  margin: 0;
  padding: 0;
  position: absolute;
  top: 100%;
}
.submenu--active {
  display: block;
}

.submenu__item {
  width: 150px;
}

.submenu__link {
  background-color: lightblue;
  color: black;
  display: block;
  padding: 0.5em 1em;
  text-decoration: none;
}

.submenu__link:hover,
.submenu__link:focus {
  background-color: black;
  color: lightblue;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<ul class="menu">
  <li class="menu__item">
    <a class="menu__link" href="#menu-1">Menu 1</a>
    <ul class="submenu" id="menu-1" tabindex="-1">
      <li class="submenu__item"><a class="submenu__link" href="http://example.com/#1">Example 1</a></li>
      <li class="submenu__item"><a class="submenu__link" href="http://example.com/#2">Example 2</a></li>
      <li class="submenu__item"><a class="submenu__link" href="http://example.com/#3">Example 3</a></li>
      <li class="submenu__item"><a class="submenu__link" href="http://example.com/#4">Example 4</a></li>
    </ul>
  </li>
  <li class="menu__item">
    <a  class="menu__link" href="#menu-2">Menu 2</a>
    <ul class="submenu" id="menu-2" tabindex="-1">
      <li class="submenu__item"><a class="submenu__link" href="http://example.com/#1">Example 1</a></li>
      <li class="submenu__item"><a class="submenu__link" href="http://example.com/#2">Example 2</a></li>
      <li class="submenu__item"><a class="submenu__link" href="http://example.com/#3">Example 3</a></li>
      <li class="submenu__item"><a class="submenu__link" href="http://example.com/#4">Example 4</a></li>
    </ul>
  </li>
</ul>
lorem ipsum <a href="http://example.com/">dolor</a> sit amet.

WAI-ARIA Roles and Other Accessibility Support

This answer hopefully covers the basics of accessible keyboard and mouse support for this feature, but as it’s already quite sizable I’m going to avoid any discussion of WAI-ARIA roles and attributes, however I highly recommend that implementers refer to the spec for details on what roles they should use and any other appropriate attributes.

4

  • 42

    This is the most complete answer, with explanations and accessibility in mind. I think this should be the accepted answer since most of the other answers only handle click and are just code snippet dropped without any explanations.

    – Cyrille

    Oct 29, 2016 at 14:39

  • You don't actually want to bind click handlers. You can bind click handlers and also handle cases where users doesn’t have a mouse. It doesn’t hurt accessibility, it only adds functionality to users with a mouse. Adding functionality to one group of users doesn’t hurt the users who can’t use that functionality. You can provide more than one way of closing a diablog This is actually a pretty common logical fallacy. It’s totally fine to give a feature to one group of users even when others don’t benefit. I agree that all users should be able to have a good experience though

    – ICW

    Oct 13, 2021 at 20:56


  • @ICW, by using blur or focusout handlers, you will still fully support mouse and touch users, and it has the added benefit of supporting keyboard users. At no point did I suggest that you ought to not support mouse users.

    – zzzzBov

    Oct 13, 2021 at 21:08

  • Awesome answer!! Thank you so much

    – DarkteK

    Feb 2 at 13:19