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>
<main id="main" role="main">
<button class="modal-toggle">Open Modal</button>
</main>

<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()
;
}
})();