How to detect clicks outside of a Web Component
Published: 2021-04-15 21:07:41

---
title: How to detect clicks outside of a Web Component
published: true
date: 2021-04-10 00:00:00 UTC
tags: 
canonical_url: https://lamplightdev.com/blog/2021/04/10/how-to-detect-clicks-outside-of-a-web-component/
---

When using Web Components it's often necessary to be able to distinguish between clicks inside the component from those outside of it.

For example you may want to show some content when a button is clicked, and then hide it when the user clicks outside the component but not when the user clicks inside the component:

![Screencast showing a button opening a component, clicking of buttons inside the component that do not close it, and a click outside the component which does close it](https://lamplightdev.com/images/2021-04-10-1-7226e174.gif)

## The problem

What may seem a simple problem becomes more complicated when you consider how events are treated differently depending on whether an element is in the Shadow DOM or in the Light DOM.

Take the following component definition used in the above example:

```html
<my-component>
  #shadow-root
    <button>Open</button>
    <button>Button A</button>
    <slot><slot>

  <button>Button B</button>
</my-component>
```

The desired behaviour is:

1. close when you click outside the component
2. do not close when the component itself is clicked
3. do not close when an element in the Shadow DOM is clicked (Button A)
4. do not close when an element in the Light DOM is clicked (Button B)

A first attempt may be to add an event listener defined inside your component like this:

```js
document.addEventListener('click', (event) => {
  if (event.target !== this) {
    this.close();
  }
});
```

1. ✅ For a click outside the component `event.target !== this` so the component will close.
2. ✅ For a click on the component itself `event.target === this` so the component will not close.
3. ✅ For a click on Button A inside the Shadow DOM,`event.target === this` so the component will not close.
4. ❌ For a click on Button B inside the Light DOM `event.target !== this` so the component will close.

But why is `event.target === this` for elements inside the Shadow DOM, but not for elements inside the Light DOM?

## Event retargeting

For elements inside the Shadow DOM events are automatically **retargeted** to the parent component. This means that `event.target` is set to the parent component for any event originating from inside the Shadow DOM.

But for elements in the Light DOM, which are only **projected** to the inside of a component and are not physically moved there, the `event.target` remains set to the element itself.

So how do you find out if an element in a component's Light DOM was clicked?

## The solution

The `event.composedPath()` method shows you each element the event passed through from the originating element right up to the `Window` object at the top of the DOM tree. This works on the **projected DOM tree** so that the elements are included in the order that they appear when rendered rather than their physical position in the DOM:

```js
document.addEventListener('click', (event) => {
  console.log(event.composedPath());
  /*
    this will log an array containing the following
    when a button in the Shadow DOM is clicked:

    0: button.A
    1: div#container
    2: document-fragment
    3: my-component <--
    4: body
    5: html
    6: document
    7: Window

    and the following when a button in the Light DOM is clicked:

    0: button.B
    1: slot
    2: div#container
    3: document-fragment
    4: my-component <--
    5: body
    6: html
    7: document
    8: Window
  */
});
```

In both cases you can see that `my-component` appears in the composed path. Using this fact you can update the event listener to the following:

```js
document.addEventListener('click', (event) => {
  if (!event.composedPath().includes(this)) {
    this.close();
  }
});
```

1. ✅ For a click outside the component `composedPath()` will not include the component (`this`) so the component will close.
2. ✅ For a click on the component itself `composedPath()` will include `this` so the component will not close.
3. ✅ For a click on Button A inside the Shadow DOM,`composedPath()` will include `this` so the component will not close.
4. ✅ For a click on Button B inside the Light DOM `composedPath()` will include `this` so the component will not close.

🎆 Bingo, `composedPath()` gives you all the information you need to see where the event originated!

You can [view a full code example of the above component here](https://glitch.com/edit/#!/solar-conscious-bull?path=index.html%3A82%3A68).

[Subscribe to my mailing list to be notified of new posts about Web Components and building performant websites](http://eepurl.com/gvjFwv)