How to Detect Outside-Click with Vue 3

This article will explore How to detect outside-click with Vue 3. Sometimes, we need our components to react when users click anywhere on a web page outside a target element.

So, how can we detect outside clicks in a Vue 3 application?

There are multiple ways to detect outside clicks in Vue 3. We can implement a composable and turn it into a directive or leverage vue-use.

You can find the code for this tutorial on Github. Let’s learn the composition approach first.

Setting up a project

Before implementing the click-outside functionality, let’s set up a new Vue 3 project. The fastest way to set up a new project is create-vue.

First, run the command to create a new project:

npm create vue@latest

Then, select all the standard settings. Use Typescript if you want type-checking. Once that is done, you can delete all the necessary boilerplate code, and it is ready to go.

Using a composable

We can leverage the Vue 3 composition API to achieve a click-outside functionality.

First, create a new composable and call it useClickOutside. The composable requires three parameters:

  • The component we want to react to any clicks outside of it.
  • A callback function should be executed when clicking.
  • An exclude component that we also don’t want to respond to when we click it.
export default function useClickOutside(
  component,
  callback,
  excludeComponent
) {
  // fail early if any of the required params is missing
  if (!component) {
    throw new Error('A target component has to be provided.')
  }

  if (!callback) {
    throw new Error('A callback has to be provided.')
  }
}

Next, we have to listen to click events on the window object and run the callback when the clicking happens outside of the target component. Furthermore, we have to start listening when the component mounts.

import { onMounted } from 'vue'

export default function useClickOutside(
  component,
  callback,
  excludeComponent
) {
  // fail early if any of the required params is missing
  if (!component) {
    throw new Error('A target component has to be provided.')
  }

  if (!callback) {
    throw new Error('A callback has to be provided.')
  }

  const listener = (event) => {
    if (
      event.target === component.value ||
      event.composedPath().includes(component.value) ||
      event.target === excludeComponent.value ||
      event.composedPath().includes(excludeComponent.value)
    ) {
      return
    }
    if (typeof callback === 'function') {
      callback()
    }
  }

  onMounted(() => {
    window.addEventListener('click', listener)
  })
}

Inside the click event handler, we are checking that the current target element we clicked on is not the target component, nor the excluded one. If tha’ts the case then we call the callback function.

Finally, we don’t want memory leaks, thus, we have to clean up this event when the component unmounts.

import { onBeforeUnmount, onMounted } from 'vue'

export default function useClickOutside(
  component,
  callback,
  excludeComponent
) {
  // ...code  

  onMounted(() => {
    window.addEventListener('click', listener)
  })

  onBeforeUnmount(() => {
    window.removeEventListener('click', listener)
  })
}

Let’s use the composable inside a Vue component. We will use a Container component inside our App component for demonstration purposes.

<script setup>
import useClickOutside from '../composables/useClickOutside'
import { ref } from 'vue'

const componentRef = ref()
const excludeRef = ref()
const isOpen = ref(true)

useClickOutside(
  componentRef,
  () => {
    isOpen.value = false
  },
  excludeRef
)
</script>

<template>
  <div class="container">
    <button ref="excludeRef" @click="isOpen = true">Open Text</button>

    <div v-if="isOpen" ref="componentRef" class="text">Text</div>
  </div>
</template>

Here, we call our useClickOutside composable and pass a div containing text. We chose the button as an excluded component, so the text only disappears when clicked anywhere else but the text and the button.

Using a directive

We can leverage Vue 3 directives instead of importing and calling a composable function. It would look like this.

const VOnClickOutside = {
  mounted: function (element, binding) {
    element.clickOutsideEvent = function (event) {
      const excludeComponent = document.getElementById(binding.arg)

      if (
        !(element == event.target || element.contains(event.target)) &&
        !(
          excludeComponent &&
          (event.target == excludeComponent || excludeComponent.contains(event.target))
        )
      ) {
        binding.value(event, element)
      }
    }
    document.addEventListener('click', element.clickOutsideEvent)
  },
  unmounted: function (element) {
    document.removeEventListener('click', element.clickOutsideEvent)
  }
}

export default clickOutside

This directive is very similar in functionality to the composable we implemented previously. We add a click event listener when the component mounts and executes the desired function. To also have elements that do not trigger the click event, we check for arguments where we pass elements that should be excluded.

import { clickOutside as vClickOutside } from '../directives/clickOutside'
import { ref } from 'vue'

const isOpen = ref(true)

const closeElement = () => {
  isOpen.value = false
}
</script>

<template>
  <div class="container">
    <div>The Container.vue</div>

    <button @click="isOpen = true" id="button">Open Text</button>

    <div v-if="isOpen" v-click-outside:button="closeElement" class="text">Text</div>
  </div>
</template>

Using vue-use vOnClickOutside

Notice how in the previous example we did not directly add this functionality to components but to HTML elements? That is because handling components is different in syntax than handling HTML elements.

While we could, for instance, access the component directly in the composable example with componentRef.value.$el, it would increase the amount of code and extra checks we have to make. This is where vue-use comes in handy.

Vue-use allows us to add click-outside functionality to components and HTML elements while excluding elements without extra configuration.

To use it, we have to npm install @vueuse/components

<script setup>
import { vOnClickOutside } from '@vueuse/components'
import { ref } from 'vue'
import TheText from './TheText.vue'

const isOpen = ref(true)

const closeElement = () => {
  isOpen.value = false
}
</script>

<template>
  <div class="container">
    <div>The Container.vue</div>

    <button @click="isOpen = true" id="button">Open Text</button>

    <TheText v-if="isOpen" v-on-click-outside="closeElement" />
  </div>
</template>

Here, we can add the vOnClickOutside directive directly on a component. We also don’t have to mark the button as an excluded component. Vue-use automatically handles that for us.

Conclusion

In this article, we learned How to detect outside-click with Vue 3. To achieve that, we implemented our own composable and directive. These two approaches work and can be extended to add extra functionality.

Installing a yet another library just for a small functionality might not be ideal. However, since vue-use handles all our edge cases out of the box and is tree-shakeable it won’t be a problem if your project can tree-shake.

Martin Karsten
Martin Karsten
Articles: 1