The Incredible Accessible Modal Dialog

Update: Read about version 2 of this demonstration.

Just take me to the working model

Let me start by saying I originally created my demonstration page as an internal-only training document over a year ago. I left it untouched for all of that time until recently when it got mentioned on Twitter and started getting some attention. I was then shocked and humbled to find out that it was the number one result for a Google search on “accessible modal window”. After discovering that, I felt morally obliged to make the presentation of it semi-respectable, so I went back and updated it to reflect changes in screen reader support and created this more in-depth post.

So what is a modal dialog, or as they are also called, a modal window? A modal dialog is a window that pops up over top of another window where you must first interact with the new window before you can go back to the previous window. In Web pages, a modal dialog usually is a smaller window that almost looks embedded inside of the larger window, and the larger window is behind it and usually grayed out some. Additionally, users will not be able to click on the larger window to accidentally interact with it. If you’re still not sure what it is, it might be easier to just see it in action.

So What’s the Big Deal?

So what’s the big deal with modal dialogs? The problem is that they work great when you interact with your computer using a mouse, but if you only use a keyboard or if you use a screen reader, they can often be traps of insanity in terms of trying to figure out what is going on. When designing a modal window, there is usually only one main requirement in most implementations – the modal window should be the only thing the user can interact with on the current Web page.

There might be some other tweaks, like the ability to move or resize the modal window, or automatically setting the keyboard focus to a certain element in the modal window, but the fundamental principle is that the user can only interact with that single object within the Web page.

The Case of the Forgotten Keyboard-Only User

A lot of Web design is very mouse-centric and the fact that there are people that only use a keyboard for interacting with a computer is forgotten. Most modal dialogs are designed to keep mouse users from clicking on the parts of the page that are “off limits”, but sometimes designers don’t think of people using the Tab key to navigate to different focusable elements. If the keyboard focus isn’t trapped inside of the modal dialog, the user can begin interacting with the page behind the modal dialog. At this point, it’s anyone’s guess what is going to happen. I’ve seen cases like this where, even though the modal dialog was open, I was able to fully interact with the main window and execute functions, and I’ve seen cases where I could navigate the main window but when I tried to interact with elements, nothing would actually happen. Regardless of what happens, it’s usually difficult, if not impossible, for the user to find their way back to the modal dialog in order to dismiss it.
Lesson #1: Trap the keyboard focus within the modal window.

The Case of the Forgotten Screen Reader User

If you haven’t worked with screen readers before you might not know exactly how they interact with Web pages. Most people are used to having two different cursors on their screen that let them interact with the page – a mouse cursor and a keyboard cursor. Screen readers have a third type of “cursor” called the virtual cursor. (Different screen readers call it different things, but they all basically do the same thing.) This is separate from the mouse cursor and the keyboard cursor. You can think of the virtual cursor almost like a “eye” cursor – it is positioned where the user wants to read on the page. You will never see the virtual cursor on the page like you do the keyboard cursor. It’s an invisible pointer that allows the screen reader user to read text on the Web page. As they read down the page, the virtual cursor moves with them.

So what do you do with a modal dialog? You can keep the mouse cursor trapped in the modal window, and you can keep the keyboard focus trapped in there, but how do you trap the virtual cursor? In other words, how do you prevent someone from even reading the portions of the page that they should not be able to interact with? It’s like asking how do you keep a sighted mouse user from reading the text in that grayed out area?

It’s an important question because if you want your user to only be interacting with the content in the modal window, you don’t want them off trying to read other parts of the page that aren’t even active. In a case like this, the screen reader user might have a modal dialog popped up in front of them, but they could be happily reading along on another part of the page, oblivious to the modal dialog and any information it is presenting or asking them for. Or worse, they could be reading in the modal window and their virtual cursor could “escape” the modal window and start reading the page behind the modal window and the user wouldn’t have any idea what was going on. Fortunately, screen reader software has recently added support for dealing with this situation.
Lesson #2: Trap the virtual cursor within the modal window.

Designing and Accessible Modal Window

When you look at the ARIA spec, which defines ways to describe rich UI elements accessibly, you find something that says role=”dialog”, so the thought is maybe if I apply that ARIA attribute to my modal window all of my problems will be solved. Unfortunately, no. ARIA attributes do not perform magic.

a greyhound dog dressed in an AT-AT costume from Star Wars

<dog role=”AT-AT”></dog> does not turn this dog into an AT-AT. Photo Credit

You can apply the role=”dialog” but you are still responsible for implementing all of the functionality to make it into an actual modal dialog.

So what all goes into designing an accessible modal dialog? The W3C outlines what is needed to make a modal dialog accessible. There are some finer details, but the basics are as follows.

  • The first focusable item in the modal dialog should receive the keyboard focus.
  • The window behind the modal dialog should not be allowed to be clicked on
  • The modal dialog must trap the keyboard focus inside the modal dialog so the user can’t accidentally interact with the window behind the modal dialog.
    • When the user is on the last focusable item and presses Tab, the user should be taken to the first focusable item in the modal dialog.
    • When the user is on the first  focusable item and presses Shift-Tab, the user should be taken to the last focusable item in the modal dialog.
  • The position of the keyboard focus before the modal window opens must be saved, and the focus must be restored to this location after the modal dialog closes.

Implementing an Accessible Modal Dialog

The Basic Page Structure

First off, I’m lazy, and I like to find easy ways of doing things. To make things easy to manage I broke the page into three sections: the main page, the modal dialog, and the overlay that prevents users from clicking on the parent window but still lets them partially see it.

<div id="mainPage”></div>
<div id="modal" role="dialog"></div>
<div id="modalOverlay" tabindex="-1"></div>

With each of the modal areas I defined some CSS.

#modal {
  width:50%;
  margin-left:auto;
  margin-right:auto;
  padding: 5px;
  border: thin #000 solid;
  background-color:#fff;
  z-index:3; /* places the modal on top of everything */
  position:fixed;
  top:25%;
  left:25%;
  display:none;
}

#modalOverlay {
  width:100%;
  height:100%;
  z-index:2; /* places the modalOverlay between the main page and the modal dialog */
  background-color:#000;
  opacity:0.5;
  position:fixed;
  top:0;
  left:0;
  display:none;
  margin:0;
  padding:0;
}

When the user presses a button to make the modal window pop up, I simply switch the display:none property to display:block, and when the modal gets closed I switch them back. Note, I don’t apply display:none to the main part of the document, just the modal window. If I applied it to the main page, when the modal dialog opens, the user wouldn’t be able to see the grayed out portion of the page. There would just be a blank page behind the modal window. While this would actually help out some users like screen reader users, the visual change would be quite jarring to many users. Nonetheless, the standard way to present these windows is to gray out the main window and not to make it disappear, so I’ll solve that problem instead of solving problems that don’t actually exist.

Setting and Trapping the Keyboard Focus

The next thing I want to do is set the keyboard focus to the first focusable item in the modal dialog. That’s easy enough to do, but there are a couple of other things I need to do along the way. I need to remember where the user was before the modal dialog was opened so I can take them back there. If you are a mouse user this might not seem like a big deal, but if you depend on your keyboard alone for navigation, this is a huge deal. Imagine having to retab through dozens of links every time a modal dialog closes just to get back to where you were before.

Like I said earlier, I’m lazy, so enter jQuery.

// save current focus
focusedElementBeforeModal = jQuery(':focus');

When I close the modal dialog, I can set the user back to where they came from with the following.

// set focus back to element that had it before the modal was opened
focusedElementBeforeModal.focus();

Now the next challenge is to set the keyboard focus on the first focusable element and to also keep the keyboard focus within the bounds of the modal dialog. I’m actually going to solve both problems at the same time. Thinking through the problem you realize that you need to know what all focusable elements are in the modal window and then you need to direct the user to each next and previous element each time they press Tab or Shift-Tab. At this point images of tabindex might be dancing through your head. It’s definitely easy to implement, but please wake up from that nightmare.

Again, I’m lazy, so I don’t want to have to figure out each time I create a new modal window what elements are focusable. I want something more robust so that I don’t have to recalculate all of my tabindex values each time I update the contents of a modal dialog. And what if the contents of the modal dialog dynamically change. What then?

The W3C provides some pseudo code for how to handle the logic of all of this. I basically implemented that algorithm, except instead of prepopulating a list of focusable elements, I use jQuery to determine what elements within a modal are focusable. I recalculate this every time the user presses Tab or Shift-Tab. Every time the Tab key is pressed I execute the following.

var focusableElementsString ="a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, *[tabindex], *[contenteditable]";

function trapTabKey(obj,evt) {

  // if tab or shift-tab pressed
  if ( evt.which == 9 ) {

    // get list of all children elements in given object
    var o = obj.find('*');

    // get list of focusable items
    var focusableItems;
    focusableItems = o.filter(focusableElementsString).filter(':visible')

    // get currently focused item
    var focusedItem;
    focusedItem = jQuery(':focus');

    // get the number of focusable items
    var numberOfFocusableItems;
    numberOfFocusableItems = focusableItems.length

    // get the index of the currently focused item
    var focusedItemIndex;
    focusedItemIndex = focusableItems.index(focusedItem);

    if (evt.shiftKey) {
      //back tab
      // if focused on first item and user preses back-tab, go to the last focusable item
      if(focusedItemIndex==0){
        focusableItems.get(numberOfFocusableItems-1).focus();
        evt.preventDefault();
      }

    } else {
      //forward tab
      // if focused on the last item and user preses tab, go to the first focusable item
      if(focusedItemIndex==numberOfFocusableItems-1){
        focusableItems.get(0).focus();
        evt.preventDefault();
      }
    }
  }
}

To set the initial focus for when the modal first opens I simply call the following.

function setInitialFocusModal(obj){
  // get list of all children elements in given object
  var o = obj.find('*');

  // set focus to first focusable item
  var focusableItems;
  focusableItems = o.filter(focusableElementsString).filter(':visible').first().focus();
}

Keeping Screen Readers from Reading What They Shouldn’t Be Reading

Now I need to figure out how to keep the screen reader users from reading the portions of the page behind the modal dialog so they don’t get confused about what is happening on the page. This problem is actually the exact opposite of what we are usually trying to solve for screen reader users. Usually we are wanting to make certain text available only to screen reader users and not to other users, not hide something from screen reader users.

Until about the last year or two, the technique of hiding content from screen reader users, yet still letting others see the content was not possible in most screen readers, however, most major screen readers now support an ARIA attribute called aria-hidden which does just that. When aria-hidden=”true” that tells screen readers that they are not allowed to read or interact with that content even if it’s visible on the main page.

To implement this I simply modify my main page structure slightly to the following.

<div id="mainPage” aria-hidden="false"></div>
<div id="modal" role="dialog" aria-hidden="true"></div>
<div id="modalOverlay" tabindex="-1"></div>

In this initial state the main page can be accessed by screen readers but the modal dialog cannot. When I want to make the modal dialog visible I simply toggle the properties.

<div id="mainPage” aria-hidden="true"></div>
<div id="modal" role="dialog" aria-hidden="false"></div>
<div id="modalOverlay" tabindex="-1"></div>

Now the main page is still visible, albeit through the modal overlay, but it is hidden from screen reader users so there is no risk of screen reader users accidentally interacting with the main page while the modal dialog is open. Note, I don’t use aria-hidden on the modal overlay because the overlay doesn’t actually contain any content to interact with. It’s just a layer that changes the appearance of the page and prevents clicks on the main page.

Letting Screen Reader Users Know What Just Happened

Now I can control where all of my users are on my page, but how do I let screen reader users know that the modal dialog just opened up and what the dialog is asking for? The ARIA spec for the dialog role specifies that modal dialogs should have a dialog label either through aria-label or aria-labelledby attributes. This way, when the modal dialog receives focus the screen reader will simply announce the label for the dialog. Here is how I modified my page structure.

<div id="modal" aria-hidden="true" aria-labelledby="modalTitle modalDescription" role="dialog">
  <div id="modalDescription">Beginning of dialog window. It begins with a heading 1 called &quot;Registration Form&quot;. Escape will cancel and close the window. This form does not collect any actual information.</div>
  <h1 id="modalTitle">Registration Form</h1>
   …
</div>

In this description I’ve done a couple of things.

  1. I’ve described the structure of the modal dialog so screen reader users know how to interact with it
  2. I’ve given a brief description of what this dialog does, or in this case, doesn’t do

Building a Safety Net

So why do I bother saying that the modal dialog starts with a heading 1? There are older versions of screen readers, and some current versions, like Orca, that do not support the aria-hidden attribute, so it’s quite possible that some of my screen reader users might still be able to “escape” my modal window and start trying to interact with the main window. By telling them this information, if they do get lost somewhere else in the page, they know they can now use their heading navigation keys to quickly get back to the modal dialog.

ARIA is Great, But Performance May Vary

ARIA is a relatively new specification and browsers and screen readers are still in the process of implementing support for all of its features. This brings us to our first major problem. VoiceOver and Safari on OS X does not yet behave as the ARIA spec says it should in regards to modal dialog labels. The specification and implementation guide say that the focus should be placed on the first focusable element in a modal dialog when it opens. When that happens, the screen reader will announce to the user that they are in a dialog window and the subsequent aria-label or aria-labelled by element will be read. This works in the latest versions of JAWS, NVDA, and ChromeVox, however, the label is not announced in VoiceOver.

What I discovered though is if I make the modal dialog itself focusable (tabindex=”-1”) and then set the initial focus to the modal dialog instead of the first focusable item, then VoiceOver announces the label just fine. This isn’t really the standard way to do it so I don’t want all of my screen reader users, and non-screen reader users for that matter, to have to deal with the focus being set to the dialog itself instead of the actual first focusable item.

To solve the problem I just do a quick check to see which browser you are using, and if it’s Safari, I set the focus to the dialog instead.

var o = obj.find('*');

if(usingSafari()){
  // set a tabIndex of -1 to the modal window itself so we can set the focus on it
  jQuery('#modal').attr('tabindex','-1');

  // set the focus to the modal window itself
  obj.focus();
} else {
  // set the focus to the first keyboard focusable item
  o.filter(focusableElementsString).filter(':visible').first().focus();
}

Responding To Other Keyboard Events

The last thing I do is add support for other key presses, like Escape and Enter. Escape simply closes the dialog, like clicking the “Cancel” button, and Enter submits the form.

Putting it All Together

This tutorial shows the fundamentals that need to be considered when building an accessible modal dialog. This implementation is still fairly basic and utilitarian. To make it more robust you might want to make it so you can move or resize the modal dialog. You can view a working model of this demonstration along with all of the source code.

This entry was posted in tutorials by Greg Kraus. Bookmark the permalink.

About Greg Kraus

I am the University IT Accessibility Coordinator at North Carolina State University. I provide leadership in creating an accessible IT infrastructure by consult on the accessibility of campus projects, working with developers and content creators, provide training, and helping set policy.

4 thoughts on “The Incredible Accessible Modal Dialog

  1. You should remove the “usingSafari()” conditional. It’s not necessary. Perhaps you were testing an old version? If I spoof the IE agent string, your example works correctly.

  2. If I go back two OS versions, I can get the behavior you describe. Please make sure your OS, screen reader, and browser is up-to-date.

    • Hi James,

      I upgraded to OS X 10.8.5 (12F45), and I still experience the same issue that I did on 10.8.4. The modal window works on Safari without the shim

      http://accessibility.oit.ncsu.edu/training/aria/modal-window/no-vo-shim.html

      but there is a difference in behavior between Safari and all of the other screen readers I tested. With all of the others, the aria-labelledby attribute of the role=”dialog” is announced when the first focusable item in the dialog receives focus. For Safari I have to explicitly set the focus to the dialog itself for the aria-labelledby attribute to get read automatically.

      This could be a difference of opinion in implementation techniques. I don’t see in the ARIA spec where it is required for screen readers to announce the aria-label or aria-labelledby attributes for role=”dialog”.

      http://www.w3.org/TR/wai-aria/roles#dialog

      While the spec requires one of those attributes to be present, it doesn’t specify that the user agent should announce it.

      When I originally developed this a year and a half ago, I had to use role=”alertdialog” to implement it because of numerous screen reader support issues with role=”dialog”. With the alertdialog spec, it does seem to imply that the aria-describedby attribute should be announced.

      http://www.w3.org/TR/wai-aria/roles#alertdialog

      Perhaps I was just assuming, and maybe incorrectly, that role=”dialog” should behave similarly. Nonetheless, I think it’s helpful for overall usability of the modal dialog for screen reader users.

Comments are closed.