Situation:
I have a webpage which opens modal windows (light boxes) which contain forms where the user can input data. Users generally navigate using the keyboard, tabbing from one field to the next.
Problem:
When a modal window opens, only the window is active, the rest of the page is not accessible using the mouse, but elements can be reached by tabbing out of the modal window.
Question:
How can I restrict movement by using the tab button to only the elements within the form window?
The only thing I can think of is using Javascript to set tabindex=-1
on all form elements (and other focusable elements) when the modal window is opened and then set the tabindex
values back to their previous values when the modal window is closed.
Is there a simpler/better way?
No, it's the only way.
- Find all elements which have a
tabIndex
greater than -1
† and don't belong to your modal.
- Create an array‡ and fill it with references to each element along with its original
tabIndex
.
- Set each element's
tabIndex
to -1
so it can no longer receive focus from the keyboard.
- When the modal dialog is closed, iterate over your array and restore the original
tabIndex
.
Here's a quick demo:
function isDescendant(ancestor, descendant) {
do {
if (descendant === ancestor) return true;
} while (descendant = descendant.parentNode);
return false;
}
var tabIndexRestoreFunctions;
var lastFocused;
document.getElementById("btn-show-modal").addEventListener("click", function(e) {
lastFocused = document.activeElement;
var modal = document.querySelector(".modal");
tabIndexRestoreFunctions = Array.prototype
// get tabable items which aren't children of our modal
.filter.call(document.all, o => o.tabIndex > -1 && !isDescendant(modal, o))
// iterate over those items, set the tabIndex to -1, and
// return a function to restore tabIndex
.map(o => {
var oldTabIndex = o.tabIndex;
o.tabIndex = -1;
return () => o.tabIndex = oldTabIndex;
});
// show modal
modal.classList.add("shown");
// focus modal autofocus
modal.querySelector("[autofocus]").focus();
});
document.getElementById("btn-close-modal").addEventListener("click", function(e) {
// restore tabs
tabIndexRestoreFunctions && tabIndexRestoreFunctions.forEach(f => f());
tabIndexRestoreFunctions = null;
// hide modal
document.querySelector(".modal").classList.remove("shown");
// restore focus
lastFocused && lastFocused.focus();
});
.modal {
display: none;
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(128, 128, 128, .75);
}
.modal.shown {
display: flex;
}
.modal-content {
margin: auto;
width: 500px;
padding: 30px;
border: 1px solid #333;
background-color: #fdfdfd;
}
<label>test
<input autofocus />
</label>
<button>dummy button</button>
<hr/>
<button id="btn-show-modal">open modal</button>
<div class="modal">
<div class="modal-content">
<label>test
<input autofocus />
</label>
<button id="btn-close-modal">close modal</button>
</div>
</div>
† We look for tabIndex > -1
so that we can focus specifically on tabable elements. You could further restrict that filter to ignore hidden elements, but I'll leave that to you. In either case, the list shouldn't be very big.
‡ Alternatively, as in the demo, you could fill the array with a series of functions whose sole purpose is to reset tabIndex
. You could also forego the array entirely and simply add a data-original-tab-index
attribute to the affected elements... using document.querySelectorAll("[data-original-tab-index]")
to retrieve them after the fact.
Here's a demo which uses data attributes to store the original tabIndex
so you don't have to maintain your own array:
function isDescendant(ancestor, descendant) {
do {
if (descendant === ancestor) return true;
} while (descendant = descendant.parentNode);
return false;
}
var lastFocused;
document.getElementById("btn-show-modal").addEventListener("click", function(e) {
lastFocused = document.activeElement;
var modal = document.querySelector(".modal");
Array.prototype.forEach.call(document.all, o => {
if (o.tabIndex > -1 && !isDescendant(modal, o)) {
o.dataset.originalTabIndex = o.tabIndex;
o.tabIndex = -1;
}
});
// show modal
modal.classList.add("shown");
// focus modal autofocus
modal.querySelector("[autofocus]").focus();
});
document.getElementById("btn-close-modal").addEventListener("click", function(e) {
// restore tabs
Array.prototype.forEach.call(document.querySelectorAll("[data-original-tab-index]"), o => {
o.tabIndex = o.dataset.originalTabIndex;
delete o.dataset.originalTabIndex;
});
// hide modal
document.querySelector(".modal").classList.remove("shown");
// restore focus
lastFocused && lastFocused.focus();
});
.modal {
display: none;
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(128, 128, 128, .75);
}
.modal.shown {
display: flex;
}
.modal-content {
margin: auto;
width: 500px;
padding: 30px;
border: 1px solid #333;
background-color: #fdfdfd;
}
<label>test
<input autofocus />
</label>
<button>dummy button</button>
<hr/>
<button id="btn-show-modal">open modal</button>
<div class="modal">
<div class="modal-content">
<label>test
<input autofocus />
</label>
<button id="btn-close-modal">close modal</button>
</div>
</div>
See HTMLElement.dataset
How about catching the tab-key
? On the last element and then put the focus on the first and vice versa with shift-tab
This I am using in a multi-modless-diaolog environment, to keep the focus with in a Dialog, switching between dialogs with mouse or other key
inputs=".editing, input, textarea, button, a, select"
no_tab="[type='hidden'], :disabled"
$focusable=dlg.$form.find(inputs).not(no_tab)
$fa_first=$focusable.first()
$fa_last=$focusable.last()
$fa_last.on("keydown", (evt) =>
if evt.keyCode==9 && ! evt.shiftKey
$fa_first.focus()
evt.preventDefault()
false
)
$fa_first.on("keydown", (evt) =>
if evt.keyCode==9 && evt.shiftKey
$fa_last.focus()
evt.preventDefault()
false
)
small edit: replaced my on "unibind()" (=.off(x).on(x)) function through jQuery "on()"
Have a look at the jQuery BlockUI Plugin. They have an example using a modal box with two buttons, and it restricts tabbing as well.
It may or may not work out-of-the-box with your modal windows, but it's worth a look instead of having to implement your own work-around.
Even though it is an old post I was looking for a solution to this problem and did the following to solve it.
Using JQuery I disabled all input fields in different forms and divs as soon as the modal window opens up (except the ones on the modal form itself).
$('#formId :input').prop('disabled',true);
when the modal form is closed, you can enable the input elements again.
Disabled fields are not considered when "tabbing" around your page.
in case you want to restrict the focus inside dom "parent"
parent.addEventListener('focusout', function(event) {
event.stopPropagation();
if (node.contains(event.relatedTarget)) { // if focus moved to another
parent descend
return;
}
parent.focus(); // otherwise focus on parent or change to another dom
})
supported by all mordern browsers