Modal Dialog
Modal dialog
A modal dialog is a dialog that restricts the user input to the dialog, so as long as the dialog is displayed, the user cannot interact (e.g. click links or buttons) with the surrounding page content. Modal dialogs are typically displayed with a semi-transparent, grey overlay covering the surrounding page content.
Keyboard focus
On a webpage, the keyboard focus is the ability to receive direct keyboard input. The keyboard focus is important because it determines which element on the page can be activated via keyboard.
Keyboard-only users typically move the keyboard focus through the page by pressing the Tab key (and Shift + Tab keys to move backwards). For example, a user can press the Tab key to move focus to a link, then press the Enter key to activate the link. This is equivalent to clicking the link with the mouse.
Focus management
To ensure a modal dialog is keyboard accessible, the keyboard focus should be managed as follows:When the dialog opens
Move the focus to the dialog. The focus can be placed on either the overall dialog or an element inside the dialog (e.g. the dialog content element, heading, or a form control).
While the dialog is displayed
Constrain the keyboard focus inside the dialog. This means when the Tab key (or Shift + Tab keys) is pressed, the focus moves inside the dialog only and does not move to the surrounding page content.
When the dialog is closed
Move the focus back to the element that had focus before the dialog was opened.
Dialog implementation
Modal dialogs can be implemented either via ARIA markup or via the HTML <dialog> element.
ARIA dialog
In an ARIA dialog, the keyboard focus management is implemented via JavaScript.
In addition, the dialog must contain the attributes role="dialog" and aria-modal="true". This constrains the screen reader focus to the dialog content, ensuring the dialog is modal. In addition, the attributes ensure the dialog is announced as "Dialog" when initially displayed, informing users that the content is a dialog.
Adding an 'aria-label' or 'aria-labelledby' attribute to the dialog ensures it is announced with a label (e.g. "Dialog change username").
Ideally, add the attribute aria-haspopup="dialog" to the trigger button that opens the dialog. This ensures screen readers announce the button as "Opens dialog", so users know what functionality the button has before activating it.
Note that some screen readers on mobile devices do not consistently support this markup, for example, the screen reader may be able to access the page content outside the dialog. For this reason, in the example dialog provided below, we include screen reader-only text "Dialog start" and "Dialog end" in the dialog. This informs screen reader users where the dialog starts and ends.
Example dialog
This example uses the code provided below.
Example code
The following code is an example of focus management for modal dialogs.
JavaScript
//Array of dialog boxes that are currently displayed
let Dialogs
//Base value for z-index
const ZindexBase
//Variable value for z-index
let Zindex
//CSS selector for elements that can gain keyboard focus
const Fselector
//Global function
function openDialog(id,focusAfterClose){
//Get reference to <div> with role="dialog"
var dia
//Get reference to content <div> of the dialog
var con
//Ensure content div can be focused via JavaScript (but not by users pressing the Tab key)
con.setAttribute("tabindex","-1");
//Add focus sentinels to keep focus inside dialog box
//The sentinels contain text "Dialog start" and "Dialog end" to clearly communicate that the content is a dialog box
//and where the dialog box starts and ends. This can be a help when screen readers do not fully support dialog box modality
cre("span",{"tabindex":"0","class":"gotoend"},dia,false,"Dialog start");
cre("span",{"tabindex":"0","class":"gotostart"},dia,true,"Dialog end");
//If focusAfterClose is not provided as an argument, use the currently focused element
//This is where focus is placed after the dialog is (eventually) closed
if(
var focusAfterClose
}
//Add to global array
Dialogs.push([dia,focusAfterClose]);
//Ensure dialog displays above other dialogs
dia.style.zIndex
//Show dialog
dia.style.display
dia.removeAttribute("aria-hidden");
//If dialog contains an element with the 'autofocus' attribute, place focus on that
//If no such element exists, place focus on the content div in the dialog
focusOn([con.querySelector("[autofocus]"),con]);
//If this is the first dialog that is displayed, add focus management to the page
if(Dialogs.length
document.addEventListener("focus",monitorFocus,true);
}else{
//If other dialogs are stacked behind the top dialog, ensure they are completely hidden from screen readers
for(var i
Dialogs[i][0].setAttribute("aria-hidden","true");
}
}
}
//Global function
function closeDialog(id){
//Get reference to <div> with role="dialog"
var dia
//Check if the dialog is the topmost (visible) dialog. This is usually the case
if(dia
//Remove from array
var darr
//If no other dialogs are displayed,
if(Dialogs.length
//Remove the focus event handler from the document so it can have a rest
document.removeEventListener("focus",monitorFocus,true);
//Reset z-index value
Zindex
}else{
//Ensure the new topmost dialog is exposed to screen readers
Dialogs.at(
}
//Place focus on the focusAfterClose element
darr[1].focus();
//Hide dialog
darr[0].style.display
}else{
//Find dialog in array
for(var i
if(Dialogs[i][0]
//Hide the dialog
Dialogs[i][0].style.display
//Switch focusOnClose for the dialog above this dialog
Dialogs[i
//Note that the focus is not changed - it remains on the top level dialog
//Remove from array
Dialogs.splice(i,1);
break;
}
}
}
//Remove focus sentinels from dialog
var sentinels
for(var sen of sentinels){
sen.remove();
}
}
const focusOn
if(backwards){
//If this is a nodeList (e.g. from .documentQuerySelectorAll), make it into an array to make it reversible
if(arr instanceof NodeList){
arr
}
//Reverse array
arr
}
//Try to place focus on the elements, one by one
for(const el of arr){
//If el does not exist (e.g. when no 'autofocus' attribute found in dialog), does not support focus method, or is disabled
if(
continue;
}
//Attempt to place focus on element
el.focus();
//If focus is successfully placed,
if(document.activeElement
return true;
}
}
//Note, we are not using the return value at this stage
return false;
}
const monitorFocus
//Get reference to top level (visible) dialog
var darr
//If focus happened on the top level dialog or an element inside the dialog
if(darr[0].contains(ev.target)){
//If focus sentinel at top got focus, move focus to the end
if(ev.target.className
//Get all (potentially) focusable elements inside the dialog, then focus the first one possible
focusOn(darr[0].querySelectorAll(Fselector));
//If focus sentinel at end got focus, move focus to the top
}else if(ev.target.className
//Get all (potentially) focusable elements inside the dialog, then focus the last one possible
focusOn(darr[0].querySelectorAll(Fselector),true);
}
//Focus happened outside top level dialog
}else{
//Place focus back on content div in top level dialog
//Not looking for 'autofocus' attribute as this is not the initial display of the dialog
darr[0].querySelector("div").focus();
}
}
//Auxiliary function to create elements and add to DOM
const cre
var el
if(attributes){
for(const n of Object.keys(attributes)){
el.setAttribute(n,attributes[n]);
}
}
if(parent){
if(last){
parent.appendChild(el);
}else{
parent.insertBefore(el, parent.firstChild);
}
}
if(content){
el.textContent
}
//return el;
}
}
HTML
Change username
</button>
. . .
<div id="
<div>
<h2>
Change username
</h2>
<label for="
New username
</label>
<input autofocus type="
<button onclick="
Cancel
</button>
<button onclick="
Submit
</button>
</div>
</div>
CSS
position: fixed;
left:
top:
width:
height:
background-color: rgba(
}
div[role="dialog"] > div{
position: absolute;
left:
top:
transform: translate(
max-width:
max-height:
margin: auto;
padding:
background-color: white;
overflow: auto;
/*Rounded corners without scroll bar protruding*/
clip-path: inset(
}
@media only screen and (max-width:
div[role="dialog"] > div{
min-width:
}
}
body:has([role="dialog"][style*="block"]){
overflow: hidden;
}
[role="dialog"] > div:focus, [role="dialog"] > div:focus-visible{
outline:
}
[role="dialog"] span[class^="goto"]{
opacity:
}
<dialog> element
Using the HTML <dialog> element is much simpler as the keyboard focus is managed by the browser. In addition, it has better support by mobile screen readers than the ARIA dialog.
The example below uses the <dialog> element to implement the same three dialogs used in the ARIA example.
Example
HTML
Change username
</button>
. . .
<dialog id="
<div>
<h2>
Change username
</h2>
<label for="
New username
</label>
<input autofocus type="
<button onclick="
Cancel
</button>
<button onclick="
Submit
</button>
</div>
</dialog>
CSS
background-color: rgba(
}
Terms of Use
This software is being provided "as is" - without any express or implied warranty. In particular, me2 Accessibility does not make any representation or warranty of any kind concerning the reliability, quality, or merchantability of this software or its fitness for any particular purpose. In addition, me2 Accessibility does not guarantee that use of this tool will ensure the accessibility of your web content or that your web content will comply with any specific accessibility standard.
This work is licensed under a
Creative Commons License