✎ Technique: Accessible modal dialogs

Modal dialogs can enhance usability by focusing attention on a specific message that requires a user action to continue.

An accessible modal dialog is one where keyboard focus is managed properly, and the correct information is exposed to screen readers. HTML and WAI-ARIA can be used to provide the necessary semantic information, CSS the appearance and Javascript the behavior.

Example

In this modal dialog example, we’ll look at the HTML, CSS and JavaScript separately.

HTML

The dialog itself must be constructed from a combination of HTML and WAI-ARIA attributes, as in this example:


<div id="dialog" role="dialog" aria-labelledby="title" aria-describedby="description">
  <h1 id="title">Title of the dialog</h1>
  <p id="description">Information provided by the dialog.</p>
  <button id="close" aria-label="close">×</button>
</div>

Note the dialog role, which tells assistive technologies that the element is a dialog. The aria-labelledby and aria-describedby attributes are “relationship” attributes that connect the dialog to its title and description explicitly. So when focus is moved to the dialog or inside it, the text within those two elements will be read in succession. The close button has an aria-label attribute that overrides the element’s text character of “times” to read “close” when screen readers interact with it.

CSS

As well as some CSS for color and positioning, the dialog is set to display:none by default. When the custom attribute open is added to the dialog with JavaScript, the dialog is revealed.


[role="dialog"][data-open] {
  display: block;
}

Distinctive focus styles are added for the dialog’s the opening and closing buttons so that it’s clear to keyboard users which element is focused. This style is paired with the hover style so that keyboard and mouse operation look consistent.


button:focus, button:hover  {
  outline: 3px solid #d4aa00;
}

JavaScript

When the “trigger” button is pressed, the script runs the openDialog() function:


function openDialog() {
  dialog.setAttribute('data-open', '');
  close.focus();
  close.addEventListener('keydown', function(e) {
    if (e.keyCode == 9) {
      e.preventDefault();
    }
  });
  document.getElementById('cover').style.display = 'block';
  document.addEventListener('keydown', addESC);
}

First, dialog.setAttribute('data-open', '') adds the open attribute to the dialog, which sets the dialog’s CSS display value to block. Next, focus is moved to the dialog’s close button. This does two things: It triggers the announcement of the dialog's title and description in screen readers, and it makes the dialog easy to close at the press of a key.

The next block confines focus to the close button, making sure the user does not accidentally leave the dialog until it has been dismissed. That is, if the TAB key is pressed—which would move focus away from the dialog—the default behavior is suppressed with e.preventDefault();.

Note the line with "addESC" at the end. That adds a listener for the ESC key so that when it is pressed, closeDialog() is run. It is conventional to be able to close a dialog with the ESC key.


var addESC = function(e) {
  if (e.keyCode == 27) {
    closeDialog();
  } 
}

Clicking or pressing ENTER on the close button will also fire closeDialog():


function closeDialog() {
  dialog.removeAttribute('data-open');
  trigger.focus();      
  document.getElementById('cover').style.display = 'none';
  document.removeEventListener('keydown', addESC);
}

Note the trigger.focus() line that moves focus back to whichever element opened the dialog. This is important: Keyboard users should always be returned to where they were before they opened the dialog. If you don't do that, when the close button is hidden (and no longer focusable) the <body> element will be focused by default. That will force keyboard users to step through the page manually to find the spot where they left off.

Code editor

Try operating the dialog with only your keyboard in the code editor provided (external link). See how the keyboard operation is affected when you remove the JavaScript lines that manage focus!