How To Make Modal Windows Better For Everyone

About The Author

Scott O’Hara is a UX designer & developer based out of Boston Massachusetts. He loves pushing the limits of CSS, designing usable experiences for … More about Scott ↬

Email Newsletter

Weekly tips on front-end & UX.
Trusted by 200,000+ folks.

Depending on how a user browses the Internet, modal windows can be downright confusing. Modals quickly shift visual focus from one part of a website or application to another area of content. This scenario is more common than it should be. And it’s fairly easy to solve, as long as you make your content accessible to all through sound usability practices. In this article, Scott O’Hara has set up a demo of an inaccessible modal window that appears on page load and that isn’t entirely semantic. First, interact with it using your mouse to see that it actually works. Then, try interacting with it using only your keyboard.

To you, modal windows might be a blessing of additional screen real estate, providing a way to deliver contextual information, notifications and other actions relevant to the current screen. On the other hand, modals might feel like a hack that you’ve been forced to commit in order to cram extra content on the screen. These are the extreme ends of the spectrum, and users are caught in the middle. Depending on how a user browses the Internet, modal windows can be downright confusing.

Modals quickly shift visual focus from one part of a website or application to another area of (hopefully related) content. The action is usually not jarring if initiated by the user, but it can be annoying and disorienting if it occurs automatically, as happens with the modal window’s evil cousins, the “nag screen” and the “interstitial.”

However, modals are merely a mild annoyance in the end, right? The user just has to click the “close” button, quickly skim some content or fill out a form to dismiss it.

Well, imagine that you had to navigate the web with a keyboard. Suppose that a modal window appeared on the screen, and you had very little context to know what it is and why it’s obscuring the content you’re trying to browse. Now you’re wondering, “How do I interact with this?” or “How do I get rid of it?” because your keyboard’s focus hasn’t automatically moved to the modal window.

This scenario is more common than it should be. And it’s fairly easy to solve, as long as we make our content accessible to all through sound usability practices.

For an example, I’ve set up a demo of an inaccessible modal window that appears on page load and that isn’t entirely semantic. First, interact with it using your mouse to see that it actually works. Then, try interacting with it using only your keyboard.

Better Semantics Lead To Better Usability And Accessibility

Usability and accessibility are lacking in many modal windows. Whether they’re used to provide additional actions or inputs for interaction with the page, to include more information about a particular section of content, or to provide notifications that can be easily dismissed, modals need to be easy for everyone to use.

To achieve this goal, first we must focus on the semantics of the modal’s markup. This might seem like a no-brainer, but the step is not always followed.

Suppose that a popular gaming website has a full-page modal overlay and has implemented a “close” button with the code below:

<div id="modal_overlay">
  <div id="modal_close" onClick="modalClose()">
    X
  </div>
  …
</div>

This div element has no semantic meaning behind it. Sighted visitors will know that this is a “close” button because it looks like one. It has a hover state, so there is some visual indication that it can be interacted with.

But this element has no inherit semantic meaning to people who use a keyboard or screen reader.

There’s no default way to enable users to tab to a div without adding a tabindex attribute to it. However, we would also need to add a :focus state to visually indicate that it is the active element. That still doesn’t give screen readers enough information for users to discern the element’s meaning. An “X” is the only label here. While we can assume that people who use screen readers would know that the letter “X” means “close,” if it was a multiplication sign (using the HTML entity &times;) or a cross mark (), then some screen readers wouldn’t read it at all. We need a better fallback.

We can circumvent all of these issues simply by writing the correct, semantic markup for a button and by adding an ARIA label for screen readers:

<div id="modal_overlay">
  <button type="button" class="btn-close" id="modal_close" aria-label="close">
    X
  </button>
</div>

By changing the div to a button, we’ve significantly improved the semantics of our “close” button. We’ve addressed the common expectation that the button can be tabbed to with a keyboard and appear focused, and we’ve provided context by adding the ARIA label for screen readers.

That’s just one example of how to make the markup of our modals more semantic, but we can do a lot more to create a useful and accessible experience.

Making Modals More Usable And Accessible

Semantic markup goes a long way to building a fully usable and accessible modal window, but still more CSS and JavaScript can take the experience to the next level.

Including Focus States

Provide a focus state! This obviously isn’t exclusive to modal windows; many elements lack a proper focus state in some form or another beyond the browser’s basic default one (which may or may not have been cleared by your CSS reset). At the very least, pair the focus state with the hover state you’ve already designed:

.btn:hover, .btn:focus {
  background: #f00;
}

However, because focusing and hovering are different types of interaction, giving the focus state its own style makes sense.

.btn:hover {
  background: #f00;
}

:focus {
  box-shadow: 0 0 3px rgba(0,0,0,.75);
}

Really, any item that can be focused should have a focus state. Keep that in mind if you’re extending the browser’s default dotted outline.

Saving Last Active Element

When a modal window loads, the element that the user last interacted with should be saved. That way, when the modal window closes and the user returns to where they were, the focus on that element will have been maintained. Think of it like a bookmark. Without it, when the user closes the modal, they would be sent back to the beginning of the document, left to find their place. Add the following to your modal’s opening and closing functions to save and reenable the user’s focus.

var lastFocus;

function modalShow () {
  lastFocus = document.activeElement;
}

function modalClose () {
  lastFocus.focus(); // place focus on the saved element
}

Shifting Focus

When the modal loads, focus should shift from the last active element either to the modal window itself or to the first interactive element in the modal, such as an input element. This will make the modal more usable because sighted visitors won’t have to reach for their mouse to click on the first element, and keyboard users won’t have to tab through a bunch of DOM elements to get there.

var modal = document.getElementById('your-modal-id-here');

function modalShow () {
   modal.setAttribute('tabindex', '0');
   modal.focus();
}

Going Full Screen

If your modal takes over the full screen, then obscure the contents of the main document for both sighted users and screen reader users. Without this happening, a keyboard user could easily tab their way outside of the modal without realizing it, which could lead to them interacting with the main document before completing whatever the modal window is asking them to do.

Use the following JavaScript to confine the user’s focus to the modal window until it is dismissed:

function focusRestrict ( event ) {
  document.addEventListener('focus', function( event ) {
    if ( modalOpen && !modal.contains( event.target ) ) {
      event.stopPropagation();
      modal.focus();
    }
  }, true);
}

While we want to prevent users from tabbing through the rest of the document while a modal is open, we don’t want to prevent them from accessing the browser’s chrome (after all, sighted users wouldn’t expect to be stuck in the browser’s tab while a modal window is open). The JavaScript above prevents tabbing to the document’s content outside of the modal window, instead bringing the user to the top of the modal.

If we also put the modal at the top of the DOM tree, as the first child of body, then hitting Shift + Tab would take the user out of the modal and into the browser’s chrome. If you’re not able to change the modal’s location in the DOM tree, then use the following JavaScript instead:

var m = document.getElementById('modal_window'),
    p = document.getElementById('page');

// Remember that <div id="page"> surrounds the whole document,
// so aria-hidden="true" can be applied to it when the modal opens.

function swap () {
  p.parentNode.insertBefore(m, p);
}

swap();

If you can’t move the modal in the DOM tree or reposition it with JavaScript, you still have other options for confining focus to the modal. You could keep track of the first and last focusable elements in the modal window. When the user reaches the last one and hits Tab, you could shift focus back to the top of the modal. (And you would do the opposite for Shift + Tab.)

A second option would be to create a list of all focusable nodes in the modal window and, upon the modal firing, allow for tabbing only through those nodes.

A third option would be to find all focusable nodes outside of the modal and set tabindex=“-1” on them.

The problem with these first and second options is that they render the browser’s chrome inaccessible. If you must take this route, then adding a well-marked “close” button to the modal and supporting the Escape key are critical; without them, you will effectively trap keyboard users on the website.

The third option allows for tabbing within the modal and the browser’s chrome, but it comes with the performance cost of listing all focusable elements on the page and negating their ability to be focused. The cost might not be much on a small page, but on a page with many links and form elements, it can become quite a chore. Not to mention, when the modal closes, you would need to return all elements to their previous state.

Clearly, we have a lot to consider to enable users to effectively tab within a modal.

Dismissing

Finally, modals should be easy to dismiss. Standard alert() modal dialogs can be closed by hitting the Escape key, so following suit with our modal would be expected — and a convenience. If your modal has multiple focusable elements, allowing the user to just hit Escape is much better than forcing them to tab through content to get to the “close” button.

function modalClose ( e ) {
  if ( !e.keyCode || e.keyCode === 27 ) {
    // code to close modal goes here
  }
}

document.addEventListener('keydown', modalClose);

Moreover, closing a full-screen modal when the overlay is clicked is conventional. The exception is if you don’t want to close the modal until the user has performed an action.

Use the following JavaScript to close the modal when the user clicks on the overlay:

mOverlay.addEventListener('click', function( e )
  if (e.target == modal.parentNode)
    modalClose( e );
  }
}, false);

Additional Accessibility Steps

Beyond the usability steps covered above, ARIA roles, states and properties will add yet more hooks for assistive technologies. For some of these, nothing more is required than adding the corresponding attribute to your markup; for others, additional JavaScript is required to control an element’s state.

aria-hidden

Use the aria-hidden attribute. By toggling the value true and false, the element and any of its children will be either hidden or visible to screen readers. However, as with all ARIA attributes, it carries no default style and, thus, will not be hidden from sighted users. To hide it, add the following CSS:

.modal-window[aria-hidden="true"] {
  display: none;
}

Notice that the selector is pretty specific here. The reason is that we might not want all elements with aria-hidden=“true” to be hidden (as with our earlier example of the “X” for the “close” button).

role=“dialog”

Add role=“dialog” to the element that contains the modal’s content. This tells assistive technologies that the content requires the user’s response or confirmation. Again, couple this with the JavaScript that shifts focus from the last active element in the document to the modal or to the first focusable element in the modal.

However, if the modal is more of an error or alert message that requires the user to input something before proceeding, then use role=“alertdialog” instead. Again, set the focus on it automatically with JavaScript, and confine focus to the modal until action is taken.

aria-label

Use the aria-label or aria-labelledby attribute along with role=“dialog”. If your modal window has a heading, you can use the aria-labelledby attribute to point to it by referencing the heading’s ID. If your modal doesn’t have a heading for some reason, then you can at least use the aria-label to provide a concise label about the element that screen readers can parse.

What About HTML5’s Dialog Element?

Chrome 37 beta and Firefox Nightly 34.0a1 support the dialog element, which provides extra semantic and accessibility information for modal windows. Once this native dialog element is established, we won’t need to apply role=“dialog” to non-dialog elements. For now, even if you’re using a polyfill for the dialog element, also use role=“dialog” so that screen readers know how to handle the element.

The exciting thing about this element is not only that it serves the semantic function of a dialog, but that it come with its own methods, which will replace the JavaScript and CSS that we currently need to write.

For instance, to display or dismiss a dialog, we’d write this base of JavaScript:

var modal = document.getElementById('myModal'),
  openModal = document.getElementById('btnOpen'),
  closeModal = document.getElementById('btnClose');

// to show our modal
openModal.addEventListener( 'click', function( e ) {
  modal.show();
  // or
  modal.showModal();
});

// to close our modal
closeModal.addEventListener( 'click', function( e ) {
  modal.close();
});

The show() method launches the dialog, while still allowing users to interact with other elements on the page. The showModal() method launches the dialog and prevents users from interacting with anything but the modal while it’s open.

The dialog element also has the open property, set to true or false, which replaces aria-hidden. And it has its own ::backdrop pseudo-element, which enables us to style the modal when it is opened with the showModal() method.

There’s more to learn about the dialog element than what’s mentioned here. It might not be ready for prime time, but once it is, this semantic element will go a long way to helping us develop usable, accessible experiences.

Where To Go From Here?

Whether you use a jQuery plugin or a homegrown solution, step back and evaluate your modal’s overall usability and accessibility. As minor as modals are to the web overall, they are common enough that if we all tried to make them friendlier and more accessible, we’d make the web a better place.

I’ve prepared a demo of a modal window that implements all of the accessibility features covered in this article.

Further Reading

Smashing Editorial (hp, il, al, ml, mrn)