Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Vue.js 2.7: Detect clicks outside an element (Close modals, popups, etc.)

Detecting outside clicks in Vue.js

See this StackOverflow thread

First off, include the directive at the end of this gist.

  1. On your open button, make sure to use @click.stop to prevent the open click event from closing your modal.
  2. On your modal, add the v-click-outside directive and points it at a function to call when clicked outside.

Example:

<template>
  <button @click.stop="openPopup” />
  <div v-if="shouldShowModal” v-click-outside="hidePopup” />
</template>

<script>

export default {
  data() {
    return {
      shouldShowModal: false
    };
  },
  methods: {
    showPopup() {
      this.shouldShowModal = true;
    },
    hidePopup() {
      this.shouldShowModal = false;
    }
  },
};
</script>

Directive:

Based on this SO answer

Vue.directive(‘click-outside’,{
  bind: function (el, binding, vnode) {
      el.eventSetDrag = function () {
          el.setAttribute('data-dragging', 'yes');
      }
      el.eventClearDrag = function () {
          el.removeAttribute('data-dragging');
      }
      el.eventOnClick = function (event) {
          var dragging = el.getAttribute('data-dragging');
          // Check that the click was outside the el and its children, and wasn't a drag
          if (!(el == event.target || el.contains(event.target)) && !dragging) {
              // call method provided in attribute value
              vnode.context[binding.expression](event);
          }
      };
      document.addEventListener('touchstart', el.eventClearDrag);
      document.addEventListener('touchmove', el.eventSetDrag);
      document.addEventListener('click', el.eventOnClick);
      document.addEventListener('touchend', el.eventOnClick);
  }, unbind: function (el) {
      document.removeEventListener('touchstart', el.eventClearDrag);
      document.removeEventListener('touchmove', el.eventSetDrag);
      document.removeEventListener('click', el.eventOnClick);
      document.removeEventListener('touchend', el.eventOnClick);
      el.removeAttribute('data-dragging');
  },
});
@fransstudio2

This comment has been minimized.

Copy link

@fransstudio2 fransstudio2 commented Apr 4, 2020

Thanks for this. Found this page by googling.
Btw is closePopup meant to be hidePopup here? :)

@AnalyzePlatypus

This comment has been minimized.

Copy link
Owner Author

@AnalyzePlatypus AnalyzePlatypus commented Apr 4, 2020

👍 Good catch, fixed.

@igd-tom

This comment has been minimized.

Copy link

@igd-tom igd-tom commented Jul 5, 2020

Thanks for your solution, works great on Desktop! However on my Android phone browser it doesn't quite behave properly. If you click the button to open the dialog box it opens, but if you click the button again the dialog box attempts to close but then reopens again. Though clicking outside the button will close the dialog properly. Any ideas on what that could be?

@noeleo25

This comment has been minimized.

Copy link

@noeleo25 noeleo25 commented Jul 10, 2020

Thanks !! great workaround :) I was doing something similar but on each component, definitely better with this custom vue directive.

@AnalyzePlatypus

This comment has been minimized.

Copy link
Owner Author

@AnalyzePlatypus AnalyzePlatypus commented Jul 12, 2020

@igd-tom, I'd guess it's probably something off in the code that opens the dialog. Can you post a fiddle or CodeSandbox demonstrating the problem?

@kicombo

This comment has been minimized.

Copy link

@kicombo kicombo commented Jan 5, 2021

Nice work

@JoeyKinch

This comment has been minimized.

Copy link

@JoeyKinch JoeyKinch commented Mar 16, 2021

This is the ONLY one I've found that actually works, you're a gem!

@shershen

This comment has been minimized.

Copy link

@shershen shershen commented May 21, 2021

Thanks you! 👍 🎉 😎

@dbramwell

This comment has been minimized.

Copy link

@dbramwell dbramwell commented Jun 11, 2021

Update for Vue 3:

{
  beforeMount(el, binding, vnode) {
      el.eventSetDrag = function () {
          el.setAttribute('data-dragging', 'yes');
      }
      el.eventClearDrag = function () {
          el.removeAttribute('data-dragging');
      }
      el.eventOnClick = function (event) {
          var dragging = el.getAttribute('data-dragging');
          // Check that the click was outside the el and its children, and wasn't a drag
          if (!(el == event.target || el.contains(event.target)) && !dragging) {
              // call method provided in attribute value
              binding.value(event);
          }
      };
      document.addEventListener('touchstart', el.eventClearDrag);
      document.addEventListener('touchmove', el.eventSetDrag);
      document.addEventListener('click', el.eventOnClick);
      document.addEventListener('touchend', el.eventOnClick);
  },
  unmounted(el) {
      document.removeEventListener('touchstart', el.eventClearDrag);
      document.removeEventListener('touchmove', el.eventSetDrag);
      document.removeEventListener('click', el.eventOnClick);
      document.removeEventListener('touchend', el.eventOnClick);
      el.removeAttribute('data-dragging');
  }
}
@mrtrimble

This comment has been minimized.

Copy link

@mrtrimble mrtrimble commented Aug 5, 2021

Thanks! This worked perfectly!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment