119

I have searched for a good solution everywhere, yet I can't find one which does not use jQuery.

Is there a cross-browser, normal way (without weird hacks or easy to break code), to detect a click outside of an element (which may or may not have children)?

1
  • Would setting an onclick event handler on the body tag do the job? Commented Jan 7, 2013 at 1:24

8 Answers 8

282

Add an event listener to document and use Node.contains() to find whether the target of the event (which is the inner-most clicked element) is inside your specified element. It works even in IE5

const specifiedElement = document.getElementById('a')

// I'm using "click" but it works with any event
document.addEventListener('click', event => {
  const isClickInside = specifiedElement.contains(event.target)

  if (!isClickInside) {
    // The click was OUTSIDE the specifiedElement, do something
  }
})

var specifiedElement = document.getElementById('a');

//I'm using "click" but it works with any event
document.addEventListener('click', function(event) {
  var isClickInside = specifiedElement.contains(event.target);
  if (isClickInside) {
    alert('You clicked inside A')
  } else {
    alert('You clicked outside A')
  }
});
div {
  margin: auto;
  padding: 1em;
  max-width: 6em;
  background: rgba(0, 0, 0, .2);
  text-align: center;
}
Is the click inside A or outside?
<div id="a">A
  <div id="b">B
    <div id="c">C</div>
  </div>
</div>

Sign up to request clarification or add additional context in comments.

Comments

25

You need to handle the click event on document level. In the event object, you have a target property, the inner-most DOM element that was clicked. With this you check itself and walk up its parents until the document element, if one of them is your watched element.

See the example on jsFiddle

document.addEventListener("click", function (e) {
  var level = 0;
  for (var element = e.target; element; element = element.parentNode) {
    if (element.id === 'x') {
      document.getElementById("out").innerHTML = (level ? "inner " : "") + "x clicked";
      return;
    }
    level++;
  }
  document.getElementById("out").innerHTML = "not x clicked";
});

As always, this isn't cross-bad-browser compatible because of addEventListener/attachEvent, but it works like this.

A child is clicked, when not event.target, but one of it's parents is the watched element (i'm simply counting level for this). You may also have a boolean var, if the element is found or not, to not return the handler from inside the for clause. My example is limiting to that the handler only finishes, when nothing matches.

Adding cross-browser compatability, I'm usually doing it like this:

var addEvent = function (element, eventName, fn, useCapture) {
  if (element.addEventListener) {
    element.addEventListener(eventName, fn, useCapture);
  }
  else if (element.attachEvent) {
    element.attachEvent(eventName, function (e) {
      fn.apply(element, arguments);
    }, useCapture);
  }
};

This is cross-browser compatible code for attaching an event listener/handler, inclusive rewriting this in IE, to be the element, as like jQuery does for its event handlers. There are plenty of arguments to have some bits of jQuery in mind ;)

5 Comments

What if the user clicked a child of the element?
Using this solution, you check the target property (which, as metadings says, will be the inner-most DOM element) - and all its parent elements. Either you reach your watched element, which means the user clicked inside it, or you reach the document element, in which case he/she clicked outside. Alternatively, you could add two click listeners - one on the document element, and another on the watched element, which uses event.stopPropagation()/cancelBubble so that the listener on the document element is only triggered by a click outside the watched element.
That's some clever use of for. Out of curiosity, would there be any reason not to compare an element from a variable with the target on the event (using strict comparison)?
@Tiberiu-IonuțStan yes, if you create this 'watch' using a reference instead of the id 'x', I suggest you to use element === watchedElement using triple equals.
Your solution works great. This can be a personal issue, but I'd rather share it. I have the same need, but a button clicked shows a div that, when user clicks outside, should be hidden. The problem with this solution is that the div is never again shown. I forked your code here for an example: jsfiddle.net/bx5hnL2h
8

How about this:

jsBin demo

document.onclick = function(event){
  var hasParent = false;
    for(var node = event.target; node != document.body; node = node.parentNode)
    {
      if(node.id == 'div1'){
        hasParent = true;
        break;
      }
    }
  if(hasParent)
    alert('inside');
  else
    alert('outside');
} 

Comments

3

you can use composePath() to check if the click happened outside or inside of a target div that may or may not have children:

const targetDiv = document.querySelector('#targetDiv')

document.addEventListener('click', (e) => {
  const isClickedInsideDiv = e.composedPath().includes(targetDiv)

  if (isClickedInsideDiv) {
    console.log('clicked inside of div')
  } else {
    console.log('clicked outside of div')
  } 
})

Comments

0

I did a lot of research on it to find a better method. JavaScript method .contains go recursively in DOM to check whether it contains target or not. I used it in one of react project but when react DOM changes on set state, .contains method does not work. SO i came up with this solution

//Basic Html snippet
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
</head>
<body>
<div id="mydiv">
  <h2>
    click outside this div to test
  </h2>
  Check click outside 
</div>
</body>
</html>


//Implementation in Vanilla javaScript
const node = document.getElementById('mydiv')
//minor css to make div more obvious
node.style.width = '300px'
node.style.height = '100px'
node.style.background = 'red'

let isCursorInside = false

//Attach mouseover event listener and update in variable
node.addEventListener('mouseover', function() {
  isCursorInside = true
  console.log('cursor inside')
})

/Attach mouseout event listener and update in variable
node.addEventListener('mouseout', function() {
    isCursorInside = false
  console.log('cursor outside')
})


document.addEventListener('click', function() {
  //And if isCursorInside = false it means cursor is outside 
    if(!isCursorInside) {
    alert('Outside div click detected')
  }
})

WORKING DEMO jsfiddle

Comments

0

using the js Element.closest() method:

let popup = document.querySelector('.parent-element')
popup.addEventListener('click', (e) => {
   if (!e.target.closest('.child-element')) {
      // clicked outside
   }
});

1 Comment

I changed popup.addEventListener to document.addEventListener and it did the job
-2

To hide element by click outside of it I usually apply such simple code:

var bodyTag = document.getElementsByTagName('body');
var element = document.getElementById('element'); 
function clickedOrNot(e) {
	if (e.target !== element) {
		// action in the case of click outside 
		bodyTag[0].removeEventListener('click', clickedOrNot, true);
	}	
}
bodyTag[0].addEventListener('click', clickedOrNot, true);

Comments

-2

Another very simple and quick approach to this problem is to map the array of path into the event object returned by the listener. If the id or class name of your element matches one of those in the array, the click is inside your element.

(This solution can be useful if you don't want to get the element directly (e.g: document.getElementById('...'), for example in a reactjs/nextjs app, in ssr..).

Here is an example:

   document.addEventListener('click', e => {
      let clickedOutside = true;

      e.path.forEach(item => {
        if (!clickedOutside)
          return;

        if (item.className === 'your-element-class')
          clickedOutside = false;
      });

      if (clickedOutside)
        // Make an action if it's clicked outside..
    });

I hope this answer will help you ! (Let me know if my solution is not a good solution or if you see something to improve.)

4 Comments

What about nested elements?
@Tiberiu-IonuțStan you have access to the entire node list, from the parent to the children, so you can manipulate them easily.
Suddently your solution isn’t simple anymore.
@Tiberiu-IonuțStan Why ? Personally I find this solution is the simplest in a React or complicated JS environment. Don't need any ref, don't need to implement a solution if the dom make time to load, you have more control.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.