A11Y Solutions
Provides auto-suggestions when entering text

    Modals

    Also known as dialogs and popups, a modal is a user interface element intended to disable the main content area forcing user interaction before returning to the main content.

    The Problem

    Tabbing when a modal is opened will continue to tab through the content outside the modal.

    The Solution

    Use JavaScript to maintain keyboard focus inside the modal, reset to first tabbable element when the last was reached and vice versa only when the modal is open.

    Related Articles

    Live Examples

    Before illustrates the problem, After illustrates the solution. Click the header to see it larger in a modal.

    Please note that this Before example contains inaccessible code, use this link to skip to the After Live Example

    before

    after

    Code Comparison

    Code diff between the before and after examples above to show the changes necessary. To copy the final source click on the 'after' path link before the diff.

    source

    Comparing /examples/focus/modals/before/index.html to /examples/focus/modals/after/index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Accessibility Solutions - Focus - Modals</title>
    <link rel="stylesheet" href="../../../presentation.css" />
    <link rel="stylesheet" href="modals.css" />
    </head>
    <body>
    <header>
    <p>Header content with <a href="#">a link</a></p>
    </header>
    <main id="main" role="main">
    <button class="modal-toggle">Open Modal</button>
    </main>
    <footer>
    <p>Footer content with <a href="#">a link</a></p>
    </footer>

    <div
    class="modal"
    role="dialog"
    aria-labelledby="modal__label"
    aria-modal="true"
    >
    <div class="modal__body">
    <h1 id="modal__label" tabindex="-1">Modal Header</h1>
    <p>
    Lorem ipsum dolor sit amet
    <a href="#">consectetur adipisicing</a> elit. Numquam nam temporibus
    nihil dolores minima exercitationem perferendis laboriosam
    consequuntur est laborum molestiae.
    </p>
    <button class="modal__close">Close</button>
    </div>
    <div class="modal__overlay"></div>
    </div>
    <script src="modals.js"></script>
    </body>
    </html>

    styles

    Comparing /examples/focus/modals/before/modals.css to /examples/focus/modals/after/modals.css

    .modal {
    position: relative;
    }

    .modal__body {
    background-color: #fff;
    border-radius: 4px;
    display: none;
    left: 50%;
    margin-left: auto;
    margin-right: auto;
    padding: 20px;
    position: fixed;
    top: 50%;
    transform: translate(-50%, -50%);
    width: 70%;
    z-index: 10;
    }

    .modal__overlay {
    background-color: #000;
    background-color: rgba(0, 0, 0, 0.5);
    display: none;
    left: 0;
    position: fixed;
    height: 100%;
    margin: 0;
    padding: 0;
    top: 0;
    width: 100%;
    z-index: 5;
    }

    .modal h1 {
    margin-top: 0;
    }

    javascript

    Comparing /examples/focus/modals/before/modals.js to /examples/focus/modals/after/modals.js

    (function () {
    'use strict';

    let priorFocus;
    const modal = document.querySelector('.modal__body');
    const modalLabel = document.querySelector('#modal__label
    ');
    const modalOverlay = document.querySelector('.modal__overlay');
    const modalToggle = document.querySelector('.modal-toggle');
    modalToggle.addEventListener('click', openModal);

    function openModal() {
    // Track the element (likely a button) that had focus before we open the modal.
    priorFocus = document.activeElement;
    const modalClose = modal.querySelector('.modal__close');

    // Set up the event listeners we need for the modal
    modal.addEventListener('keydown', keydownEvent);
    modalOverlay.addEventListener('click', closeModal);
    modalClose.addEventListener('click', closeModal);

    // Find all focusable children
    const focusableElementsString =
    'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, [tabindex="0"], [contenteditable]';
    let focusableElements = modal.querySelectorAll(focusableElementsString);
    focusableElements = Array.prototype.slice.call(focusableElements);

    const firstTabStop = focusableElements[0];
    const lastTabStop = focusableElements[focusableElements.length - 1];

    // Show the modal and overlay
    modal.style.display = 'block';
    modalOverlay.style.display = 'block';

    modal
    Label.focus();

    function keydownEvent(e) {
    // Escape key should close the modal
    if (e.key === 'Escape') {
    closeModal();
    }

    // Tab key check for first or last tab stop
    if (e.key === 'Tab') {
    // Tab + Shift (reverse tabbing)
    if (e.shiftKey) {
    // If the current item is the first tab stop, or it shares a name with it (for example a set of radio buttons)
    if (
    document.activeElement === firstTabStop ||
    (document.activeElement.name !== '' &&
    document.activeElement.name === firstTabStop.name)
    ) {
    e.preventDefault();
    lastTabStop.focus();
    }
    } else if (document.activeElement === lastTabStop) {
    e.preventDefault();
    firstTabStop.focus();
    }
    }
    }
    }

    function closeModal() {
    // Hide the modal and overlay
    modal.style.display = 'none';
    modalOverlay.style.display = 'none'
    ;

    // Set focus back to element that had it before the modal was opened
    priorFocus.focus()
    ;
    }
    })();