Dialog (custom)

A child window of the main application (web page).
Screenshot of generic web dialog

Introduction

In traditional desktop operating systems, a dialog is a child-window that typically will either communicate information to the user, display a prompt for input, or allow the user to verify or cancel an action.

Whilst these specific cases are also valid and true on the web, we also see much more generic and custom use of the dialog (for better or worse). For example, a dialog itself might contain an entire full page-like experience, navigation or settings.

This "generic" or "custom" dialog is discussed here, and falls under the umbrella of progressive disclosure, but also familiarise yourself with the following, more specific dialog patterns:

TIP: On large screens, dialogs are typically "light-boxed" in the center or "panelled" on one edge, with a mask covering the remaining portion of the screen. On small screens however, the dialog may cover the entire screen.

Updated: March 4th, 2020

Screenshot of various fullscreen dialogs on mobile device

Working Examples

You can take a look at the generic dialog pattern in action on our examples site.

You can get an idea of the required markup structure by viewing our bones site.

Terminology

  • dialog: the pattern as a whole, comprised of the following sub parts

  • dialog button: the button that opens the child window

  • parent window: the page containing the button

  • child window: the overlay

  • title: the title of the child window

  • modality: modal or non-modal, dialogs with a mask are always modal

  • mask: an element that visibly masks the content in the parent window

Best Practices

A dialog is typically opened in one of two ways:

  • Click-activated: explicitly opened and closed by clicking the button

  • System-activated: automatically opened by the system/application on page load or at some other arbitrary time

Opening dialogs that are not requested by user (i.e. system-activated) are a violation of WCAG Guideline 3.2.5 (Level AAA) and therefore should be reserved for exceptional circumstances only (i.e. not ads!).

Overlay must not be opened on hover or focus of button (or any other element).

Overlay must be opened on click-event of button or via an application event.

Overlay must use ARIA role of dialog (not alertdialog in this context).

Overlay must contain a button that can dismiss the dialog (this will be 'close', 'cancel' or 'okay', depending on the specific type of dialog).

Overlay must contain at least one interactive element (this can be the close button).

Overlay must be labelled by an onscreen element (a heading for example) or explicit attribute.

Overlay must start heading hierarchy at level 2.

Mask must be positioned between parent window and child window.

If the overlay contains critical content, the content must be available without JavaScript. For example, the content could exist either at an alternative URL or elsewhere on the same page.

Interaction Design

This section provides interaction design for keyboard, screen reader and pointing devices.

Keyboard

Dialog button must be keyboard focusable.

If dialog button has focus, ENTER or SPACE key must open child window.

When child window opens, focus must move to the close button inside of dialog (i.e. the 'X" button).

Child window must confine TAB and SHIFT-TAB to it's focusable children.

Pressing ESC key may close child window.

Dialog button must receive focus when child window is closed.

Screen Reader

Dialog button must not announce 'has popup'. A dialog is not considered a popup (i.e aria-haspopup="true" is not valid in this context).

Title must be announced when child window opens (via focus management).

Focussed element must be announced when child window opens (via focus management).

Virtual cursor must be confined within child window.

Assistive technology might announce "Entering/leaving dialog" (or words to those affect) when focus enters or leaves the overlay respectively.

Pointer

Clicking dialog button must open child window.

Clicking mask can dismiss child window (same functionality as close button).

Developer Guide

HTML does provide a <dialog> tag, but at the time of writing this tag is experimental, and only supported in Chrome and Opera.

Content (HTML)

We are going to create a click-activated dialog. This will require a button (i.e. the thing we have to click on).

Button

We are going to add a button element that opens the dialog when clicked:

<button class="dialog-button" data-dialog="my-dialog" type="button">Open Dialog</button>

We have added a custom data attribute called data-dialog. The value of this attribute references the ID of the dialog element.

Dialog Role

The root element of the child window must have a role of dialog.

<div id="my-dialog" role="dialog">
</div>

Hidden State

The dialog must have a property of hidden to prevent it from being displayed on page load.

<div hidden id="my-dialog" role="dialog">
</div>

To display the dialog, simply remove this property. We will later show how to use this property in conjunction with CSS transitions and JavaScript.

Child Window

The child window element contains the actual dialog content.

<div hidden id="my-dialog" role="dialog">
<div class="dialog__window">
<!-- dialog content goes here -->
</div>
</div>

Header and Body

Next we create header and body structure.

<div hidden id="my-dialog" role="dialog">
<div class="dialog__window">
<header role="banner">
</header>
<div>
</div>
</div>
</div>

Title

Every dialog must be labelled. We can use aria-labelledby to point to a suitable labelling element inside of our dialog.

<div aria-labelledby="dialog_title" hidden id="my-dialog" role="dialog">
<div class="dialog__window">
<header role="banner">
<h2 id="dialog_title">Dialog Example</h2>
</header>
<div>
<!-- dialog contents go here -->
</div>
</div>
</div>

Alternatively, if no suitable heading or labelling element exists, we can specify the label in an aria-label property instead:

<div aria-label="Dialog Example" hidden id="my-dialog" role="dialog">

Close Button

Every dialog requires some form of close button. For a generic dialog, this button should be placed in the header.

<div hidden id="my-dialog" role="dialog">
<div class="dialog__window">
<header role="banner">
<h2 id="dialog_title">Dialog Example</h2>
<button aria-label="Close Dialog">X</button>
</header>
<div>
<!-- dialog contents go here -->
</div>
</div>
</div>

A more specific type of dialog may feature a different, and more prominent, way to dismiss a dialog. For example, an 'Okay' button in an alert dialog, or a 'Cancel' button in a confirmation dialog. In such cases the close button can be omitted from the header.

Presentation (CSS)

Once we have our markup in place, there are many different ways to visually, and uniquely, style a dialog.

The most important aspects for accessibility, are the mask and the hidden state.

Hidden Polyfill

First of all, let's provide a "polyfill" for older browsers that do not support the hidden attribute.

[hidden] {
display: none;
}

Dialog Mask

The mask creates a visual distinction between the overlay content in the foreground, and the page content in the background.

It must also prevent pointing devices from interacting with the page content behind the window.

.dialog {
background-color: rgba(51,51,51,0.7);
bottom: 0;
left: 0;
position: fixed;
right: 0;
top: 0;
}

NOTE: the mask does not prevent keyboard or screen reader from accessing the page content behind the window (we need JavaScript for that).

Behaviour (JS)

A dialog is made visible simply by toggling its hidden attribute. CSS animation of the dialog and mask is outside the scope of this guide.

dialog-button.js
function onDialogButtonClick() {
dialogEl.hidden = false;
}

No matter how a dialog is opened, and how it is styled, it will always have the same accessibility requirements once in the open state.

The goal is to confine keyboard focus, confine the screen reader virtual cursor, and allow the user to close the dialog and return to their previous page position.

Make it Modal

While open, a modal dialog must prevent keyboard focus moving from the child window to the parent window. In addition, keyboard focus must wrap from the last interactive element in the child window back to the first, and vice versa.

There are several different JavaScript techniques to achieve such behaviour. We provide an example JavaScript module, called makeup-keyboard-trap for you are so inclined.

Make itTruly Modal

While open, any content that is not inside of the child window must be hidden from screen reader users.

This can be achieved by applying aria-hidden="true" to any non direct-ancestor nodes in the DOM.

You must also remember to unhide these elements when the dialog is closed, otherwise the page will be completely hidden & inaccessible to screen readers!

Again, we provide an example module, called makeup-screenreader-trap, to assist with this behaviour.

Prevent Page Scroll

It is desirable to prevent the background page from scrolling when interacting with the foreground dialog content. This can be achieved by simply applying overflow:hidden to the html body:

dialog.css
body.has-modal {
overflow: hidden;
}

The presence of this class would be toggled by our JavaScript on dialog hide/show event.

ESC Key

Pressing the ESC key while focus is on the close button should also close the dialog:

dialog.js
windowEl.addEventListener('keydown', function(e) {
if (e.keyCode === 27) {
// hide the dialog
}
});

Pressing ESC key should close the dialog. Any nested widgets should take care to prevent ESC event from bubbling.

Returning Focus

When the dialog is closed, keyboard focus should move to the dialog button.

dialog-button.js
dialogWidget.addEventListener('dialog-close', function () {
dialogButtonEl.focus();
});

Here we are listening to a custom dialog-close even fired by the dialog widget.

JavaScript Modules

We have some experimental JavaScript modules that may assist you with creation of an accessible dialog widget:

ARIA Reference

This section gives an overview of ARIA usage within the context of this pattern.

role=dialog

Informs the assistive technology that the user is inside of a dialog.

role=banner

Identifies the banner/header section of the dialog. A user knows that this area typically contains the dialog heading and close button.

aria-labelledby

Informs the assistive technology of the onscreen text used to label the dialog.

aria-label

Sets an explicit label value for the dialog, overriding any onscreen labelling text.

aria-hidden

While the dialog is in an open state, aria-hidden is used to hide all non-dialog elements from assistive technology.

Changelog