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 Examplebefore
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';
modalLabel.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();
}
})();