It is often the case in web applications that designers must afford the user a progressive disclosure of complexity. One of the great ways to achieve this without disrupting the user experience is with HTML modals. The term modal means different things to different people in the web community, but when I use it here, I’m referring to exactly this: any UI element that is overlaid atop the current UI layer, regardless of whether it be a small detail or a fully enveloping layer, such that the only way to dismiss it is to either click outside of or mouse away from said modal.

There are three ways that modals can be triggered into view:

  1. Automatically. An example would be a modal that appears immediately upon logging in, that prompts a free user to upgrade to the site’s premium tier. Automatic modals should generally be avoided, although they have their rare use cases.
  2. Upon hover. Despite the popularity of this option, a compelling argument can be made that modals should never be triggered upon hover, because no UX should be triggered by hover, only UI (such as a link changing color). Here are a few reasons why triggering UX changes on hover is a bad idea:
    1. Because it increases code complexity and technical debt from a developer’s standpoint. There is no such thing as “hover” on a touchscreen device. Since most web applications must support touch devices nowadays, the code complexity great increases if an action must occur on hover for a non-touch device but on tap for a touch device.
    2. Because it is difficult to interpret user intent when hovering, but it is never difficult to determine user intent on click. When a user hovers over item B, that might be because they are simply trying to go from item A to item C. Because of this ambiguity, we have pieces of software such as the hoverIntent jQuery plugin. What these plugins do is set a timer from the moment that the user mouses over something. When the timer is up, it checks to see if the user is still mousing over that something. If the user is, it triggers the desired action, otherwise it does nothing. No such timer is necessary when the action is triggered with a click. In other words, hover is slower than click.
    3. Because a web application feels much more solid when no UX can be triggered via hover. I freely admit that this last argument is touchy-feely but I think it’s important for that very reason; ultimately we judge a UI/UX by how it makes us feel. If you are looking for an example that eschews hover UX very well, look no further than Jira. Even the drop-downs on its sales page cannot be triggered on hover; only on click. Doesn’t that have a nice feel to it?
  3. Upon click. This is the gold standard. HTML modals that appear on click are non-intrusive and they are triggered by an action that is clearly intended by the user. In situations where the modal serves as a peek into something deeper, the first click triggers the modal, and the second click sends the user to a page that contains the modal’s contents in greater detail. If Stack Overflow were to change its user modals that occur on hover to instead occur on click (if you’re unfamiliar, mouse over Ruby Velhuis to see what I am referring to), the site would be the better for it. In Ruby’s example, first click would trigger the modal, and the second click would take you to his profile.

Having thus briefly described modals, we now come to the heart of today’s subject, which is this: from a technical standpoint, how do you handle the dismissal of these modals? There are three ways to do this that I have encountered.

The first method is by initially focusing on the modal and then dismissing on the onblur method. Here’s a simple example:

<div
  class="modal"
  tabindex="-1"
  style="display:none"
  onblur="hideModal()">
  I am a modal!
</div>

<script>
  // Writing vanilla JS in real life instead of using a
  // proper framework is an abomination. Example only!
  const modal = document.querySelector('.modal');

  const showModal = () => {
    modal.style.display = 'block';
    modal.focus();
  };

  const hideModal = () => {
    modal.style.display = 'hide';
  };
</script>

This is great for very a simple modal that does not contain HTML form tags that accept focus. Envision a menu that is the visual equivalent of right-clicking on the desktop or a Finder window in macOS — that sort of simple menu. A practical web example would be the “more options” of an Instagram post when viewing it at Instagram.com. One very nice thing about this method is just how fragile focus is. It’s easily broken. You lose it as soon as you click something within the menu. You lose it if you switch to a different tab in your browser, or to a different app. This transient nature is also similar to right-clicking on macOS: switching to a different space causes the menu to disappear. The main downside to using this solution is just how limited the items inside the modal must be. If the modal contains form elements or its own set of clickable actions that upon click should not dismiss the modal, this solution won’t work.

The second method is by stopping the upward DOM propagation of click events that occur from within the modal itself, and setting a click listener on the overall document. For a generic code example, I can’t improve on this StackOverflow answer. Assume that #menucontainer is the identifier for your modal in this scenario here. When you set this up, any click events that get triggered on the document are guaranteed to have not occurred from within the modal. This is a terrible solution though, and the reason for its terribleness is pontificated quite nicely at this CSS Tricks piece. Stopping propagation of an event is never necessary and leads to many potential pitfalls. I recommend reading this CSS Tricks piece in its entirety to get a firm grasp as to exactly why this is so.1 To quote a pertinent part from it:

Modifying a single, fleeting event might seem harmless at first, but it comes with risks. When you alter the behavior that people expect and that other code depends on, you’re going to have bugs. It’s just a matter of time.

Not only is it “just a matter of time” until you have bugs with event.preventDefault(), but unless you have strenuous test coverage on your UI, this is the sort of thing that will silently break and be very difficult to debug once you discover it later, depending on how nested your DOM is. You’ll have to step through each layer of DOM nodes to see where the click event propagation is getting stopped.

This brings us to the third and gold standard for handling the dismissal of HTML modals, which is by manually detecting whether a given click occurs inside or outside the modal. The popular examples are all in jQuery but few serious web applications use jQuery, so I’ll give an example in VueJS instead, in Single File Component parlance:

<template>
  <div class="modal" v-show="showModal">I am a modal</div>
</template>

<script>
import eventHub from '../eventhub';

export default {
  name: 'Modal',
  created() {
    // this event gets sent when a click occurs abroad that
    // should open up this modal
    eventHub.$on('show-modal', this.showModal);
  },
  data() {
    return {
      showModal: false
    };
  },
  methods: {
    showModal() {
      this.showModal = true;
      document.addEventListener('click', this.documentClick);
    },
    hideModal() {
      document.removeEventListener('click', this.documentClick);
      this.showModal = false;
    },
    documentClick(e) {
      if (e.target !== this.$el &&
        !this.$el.contains(e.target)) {
        this.hideModal();
      }
    }
  }
};
</script>

For the more curious, the eventhub import is nothing more than a singleton instance of Vue, shared between all components within the application. This is standard practice. The eventhub.js file contains nothing more than this:

import Vue from 'vue';

export default new Vue();

If you’re wanting to adapt this to something other than VueJS, the main part you need to worry about in the example above is the contents of the documentClick() method. The Node.contains() method is a standard method and you can use this in any framework — React, Angular, etc. As long as you have the event object and the modal’s node object, you’re good to use this.


  1. As a bonus, this piece will also introduce you to the little-known defaultPrevented property of event objects. ↩︎