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.

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>

<div class="modal" role="dialog">
<div class="modal__body" tabindex="-1">
<h1>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>
</main>
<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';

var priorFocus;
var modal = document.querySelector('.modal__body');
var modalOverlay = document.querySelector('.modal__overlay');
var 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;
var 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
var focusableElementsString = 'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, [tabindex="0"], [contenteditable]';
var focusableElements = modal.querySelectorAll(focusableElementsString);
focusableElements = Array.prototype.slice.call(focusableElements);

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

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

modalfirstTabStop.focus();

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

// Tab key check for first or last tab stop
if (e.keyCode === 9) {

// Tab + Shift (reverse tabbing)
if (e.shiftKey) {
if (document.activeElement === firstTabStop) {
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()
;
}
}());