Accessible Dialog Box

HTML dialog boxes are created by displaying and styling page content as a dialog box (usually with lightbox effect) in the middle of the page.

Such dialog boxes are often not accessible - they might not capture the keyboard focus or it might not be clear to a screen reader user that a dialog box was opened or how to access it.

The code below is an example of an HTML dialog box with a good level of accessibility.

Try here:

When the trigger link is clicked, the following happens:

  1. The grey overlay and dialog box are made visible by switching the style to display:block;
  2. Keyboard focus is moved to the heading for the dialog box." at the top of the dialog box. This informs screen reader users that a dialog was opened (because the heading is inside a div with role="dialog'). This also places the reading point at the start of the dialog so keyboard and screen reader users can move down to read/interact with the dialog
  3. Child elements of the <body> element (except the dialog) are given the "aria-hidden" attribute

When the dialog box is closed:

  1. Child elements of the <body> element have aria-hidden values restored to previous values 
  2. Keyboard focus is moved back to the trigger link
  3. The grey overlay and dialog box is hidden by switching the style to display:none;

Modality

The dialog is modal for mouse and keyboard users, as the grey overlay prevents interaction with other page content for mouse users.

Keyboard event handlers are used to keep keyboard focus inside the dialog box. For screen reader users the modality is provided by applying the aria-hidden="true" attribute for all page content except the dialog box.

Code used

HTML

    <button onclick="openDialog('dia',this);">Test dialog</button>

    <!-- other page content -->
    <p>Lorem ipsum</p>

    <!-- dialog box at bottom of page -->
    <div id="dia" role="dialog" aria-labelledby="dia-heading" style="display:none;">
        <div role="document">
            <h1 id="dia-heading" tabindex="-1">

                Your heading text here            </h1>
            <!-- dialog content start -->

            <!-- first focusable element has class of "focus" -->
            <button class="focus">Select</button>

            <!-- dialog content end -->
            <button class="close" aria-label="Close dialog">X</button>
        </div>
    </div>
</body>

CSS


div[role="dialog"]{
    position: fixed;
    left: 0;
    top: 0;
    width: 100%;
    height:100%;
    z-index: 999999;
    background-color: rgba(0,0,0,0.6);
}

div[role="document"]{
    position: fixed;
    left:50%;
    top:50%;
    max-width:85%;
    max-height:85%;
    transform: translate(-50%, -50%);
    overflow:auto;
    border:15px solid white;
    border-radius:15px;
    padding:10px;
    background-color: white;
}

div[role="dialog"] .close {
    position:absolute;
    top: 2px;
    right: 2px;
    border:0;
    padding:0;
    cursor:pointer;
    background-color: white;
}

div[role="dialog"] h1[tabindex="0"]:focus {
    outline: 2px solid blue;
}

JavaScript

function openDialog(dialogId,lastFocused){
    var dialog = document.getElementById(dialogId);
    //It is assumed the dialog content has an h1 element at the top
    var heading = dialog.querySelector("h1");
    //lastFocused is element that had focus just before dialog was opened.
    //document.activeElement is not correct if a JAWS user has navigated
    //to a link or button via the Arrow keys.
    //Therefore, the button that triggers the dialog opening should be provided
    //as argument to this function.
    //If it's not given as argument, document.activeElement is used as fall-back
    //It will be correct in most cases
    if(!lastFocused) var lastFocused = document.activeElement;

    //Display dialog and make it perceivable to screen readers
    dialog.style.display = "block";
    //Place focus on heading inside the dialog
    heading.focus();

    function focusToDialog(ev){
        var tmp = ev.target.parentNode;
        while(tmp){
            if(tmp == dialog) return;
            tmp = tmp.parentNode;
        }
        heading.focus();
    }
    //If a keyboard user press Ctrl + L they can move to the browser address bar.
    //Pressing the Tab key will then move into the page content
    //If anything in the page content (except if inside the dialog) gets focus,
    //this event handler will move focus to the heading inside the dialog
    document.addEventListener("focus",focusToDialog,true);

    //Remove scrollbar on page to make it look nicer
    var body = document.querySelector("body");
    var bodyScroll = body.style.overflow;
    var bodyStyle = body.hasAttribute ("style");
    body.style.overflow="hidden";

    var hiddens = [];
    //Cycle through children of the body element
    for(var i=0,el;el=body.children[i];i++){
        if(el != dialog){
            //Object to remember information about each child
            var ob = {};
            //If the child already has the aria-hidden attribute, store value in object
            if(el.hasAttribute("aria-hidden")){
                ob.ariaHidden = el.getAttribute("aria-hidden");
            }
            //Set the child to be aria-hidden. This hides all content of the child from screen readers
            el.setAttribute("aria-hidden","true");
            //Add child reference to object
            ob.el = el;
            //Store object in array
            hiddens.push(ob);
        }
    }

    //Pressing the Escape key anywhere in the dialog closes the dialog
    function closeOnEscape(ev){
        if(ev.key.slice(0,3) == "Esc"){
            ev.preventDefault();
            ev.stopPropagation();
            closeDialog();
        }
    }
    dialog.addEventListener("keydown",closeOnEscape);

    function closeDialog(ev){
        //Remove all event handlers so the closure can return to the nothingness from whence it came
        document.removeEventListener("focus",focusToDialog,true);
        closeButton.removeEventListener("click",closeDialog);
        dialog.removeEventListener("keydown",closeOnEscape);
        dialog.closeDialog = null;
        closeButton.removeEventListener("keydown",wrapFocusUp);
        topFocus.removeEventListener("keydown",wrapFocusDown);
        //Reinstate the aria-hidden state to what it was when the dialog was opened
        for(var i=0,ob;ob=hiddens[i];i++){
            if(ob.ariaHidden){
                ob.el.setAttribute("aria-hidden",ob.ariaHidden);
            }else{
                ob.el.removeAttribute("aria-hidden");
            }
        }
        //Move focus back to where it was before the dialog was opened
        lastFocused.focus();
        dialog.style.display = "none";

        bodyStyle ? body.style.overflow = bodyScroll : body.removeAttribute("style");
    }
    var closeButton = dialog.querySelector(".close");
    closeButton.addEventListener("click",closeDialog);
    //Add reference to the closeDialog function. This means event handlers can close the dialog e.g. via document.getElementById('dia').closeDialog(); or this.parentNode.parentNode.closeDialog();
    dialog.closeDialog = closeDialog;

    //The first focusable element in the dialog should have the class 'focus'
    //This element is used to wrap the focus inside the dialog
    var topFocus = dialog.querySelector(".focus");
    //If no focus class is found, make heading focusable. This is not ideal, but is OK as a fall-back
    if(!topFocus) {topFocus = heading; heading.setAttribute("tabindex","0")};

    //The last focusable element in the dialog should move focus to the first focusable element
    //when the Tab key is pressed
    function wrapFocusUp(ev){
        if(ev.key == "Tab" && (!ev.shiftKey)){
            topFocus.focus();
            ev.preventDefault();
        }
    }
    closeButton.addEventListener("keydown",wrapFocusUp);

    //The first focusable element in the dialog should move focus to the last focusable element
    //when the Shift + Tab keys are pressed
    function wrapFocusDown(ev){
        if(ev.key == "Tab" && (ev.shiftKey)){
            closeButton.focus();
            ev.preventDefault();
        }
    }
    topFocus.addEventListener("keydown",wrapFocusDown);
}

Notes

Ideally the dialog box content starts with a level 1 heading. Using a level 1 heading signals that this content differs from the main page content. A level 1 heading also allows for more nested heading levels in the dialog content.

Terms of Use

Developed by Pierre Frederiksen. Pierre is a Principal Technical Consultant at Vision Australia

This software is being provided "as is", without any express or implied warranty. In particular, Vision Australia 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. additionally, Vision Australia does not guarantee that use of this software will ensure the accessibility of your web content or that your web content will comply with any specific web accessibility standard.

Creative commons licence - logo
This work is licensed under a Creative Commons License




Print Print larger font